Attest Models #26
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
| name: Attest Models | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version (e.g. v0.0.1)" | |
| required: true | |
| type: string | |
| reason: | |
| description: "Reason for this release" | |
| required: false | |
| type: string | |
| models: | |
| description: "Comma-separated model names to attest (leave empty for all)" | |
| required: false | |
| type: string | |
| ita_slot: | |
| description: "ITA policy slot override (auto = blue/green rotation)" | |
| required: false | |
| default: "auto" | |
| type: choice | |
| options: | |
| - auto | |
| - slot-a | |
| - slot-b | |
| permissions: | |
| id-token: write | |
| contents: write | |
| attestations: write | |
| packages: read | |
| env: | |
| KATA_VERSION: "3.27.0" | |
| CVM_MEASURE_REPO: "https://github.com/cohere-ai/cvm-measure.git" | |
| # TODO: remove this branch pin once the cvm-measure CLI/UKI extraction branch merges to main. | |
| CVM_MEASURE_REF: "alhassankhedr/cc-167-tdx-measurement-toolkit" | |
| PODVM_IMAGE: "ghcr.io/cohere-ai/cloud-api-adaptor/podvm" | |
| BASELINES_REPO: "cohere-ai/cohere-cc-baselines" | |
| # TODO: vendor this file into the repo once the fortress settings are stable, | |
| # then remove this runtime fetch. | |
| FORTRESS_REPO: "cohere-ai/fortress" | |
| FORTRESS_REF: "0ecb69b460b5ca9741f61a1915e7ea560a1fb1ac" | |
| FORTRESS_GENPOLICY_SETTINGS: "components/kata-policy/configs/genpolicy-settings-base.json" | |
| jobs: | |
| attest: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| # ---------------------------------------------------------------- | |
| # Setup: tools and dependencies | |
| # ---------------------------------------------------------------- | |
| - name: Install Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python dependencies | |
| run: pip install pyyaml | |
| - name: Install cvm-measure | |
| run: | | |
| git clone --depth 1 --branch "${CVM_MEASURE_REF}" "${CVM_MEASURE_REPO}" /tmp/cvm-measure | |
| pip install /tmp/cvm-measure | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Install oras | |
| uses: oras-project/setup-oras@v2 | |
| - name: Install system dependencies | |
| run: sudo apt-get update && sudo apt-get install -y mtools zstd | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v3 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | |
| - name: Configure Docker for Artifact Registry | |
| run: | | |
| gcloud auth configure-docker us-central1-docker.pkg.dev --quiet | |
| - name: Fetch genpolicy binary | |
| run: | | |
| ./scripts/fetch-genpolicy.sh \ | |
| --version "$KATA_VERSION" \ | |
| --output /usr/local/bin/genpolicy | |
| echo "GENPOLICY_VERSION=${KATA_VERSION}" >> "$GITHUB_ENV" | |
| - name: Get cvm-measure version | |
| run: | | |
| CVM_MEASURE_VERSION=$(pip show cvm-measure 2>/dev/null | grep '^Version:' | cut -d' ' -f2 || echo "unknown") | |
| echo "CVM_MEASURE_VERSION=$CVM_MEASURE_VERSION" >> "$GITHUB_ENV" | |
| # ---------------------------------------------------------------- | |
| # Resolve model list | |
| # ---------------------------------------------------------------- | |
| - name: Resolve models | |
| id: models | |
| run: | | |
| if [[ -n "${{ inputs.models }}" ]]; then | |
| MODELS="${{ inputs.models }}" | |
| else | |
| MODELS=$(ls -1 models/ | tr '\n' ',' | sed 's/,$//') | |
| fi | |
| echo "list=$MODELS" >> "$GITHUB_OUTPUT" | |
| echo "Attesting models: $MODELS" | |
| # ---------------------------------------------------------------- | |
| # Resolve per-model CVM config | |
| # ---------------------------------------------------------------- | |
| - name: Resolve per-model CVM config | |
| run: | | |
| python3 scripts/resolve-cvm-config.py \ | |
| --models "${{ steps.models.outputs.list }}" \ | |
| --models-dir models/ \ | |
| --output-dir cvm-artifacts/ | |
| # ---------------------------------------------------------------- | |
| # Fetch CVM artifacts (per model, deduplicated) | |
| # ---------------------------------------------------------------- | |
| - name: Fetch baselines | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| run: | | |
| python3 scripts/fetch-baselines.py \ | |
| --models "${{ steps.models.outputs.list }}" \ | |
| --cvm-artifacts-dir cvm-artifacts/ | |
| - name: Fetch OVMF firmware | |
| run: | | |
| python3 scripts/fetch-firmware.py \ | |
| --models "${{ steps.models.outputs.list }}" \ | |
| --cvm-artifacts-dir cvm-artifacts/ | |
| - name: Log in to GHCR | |
| run: echo "${{ github.token }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Extract UKI from podvm images | |
| run: | | |
| python3 scripts/fetch-uki.py \ | |
| --models "${{ steps.models.outputs.list }}" \ | |
| --cvm-artifacts-dir cvm-artifacts/ | |
| - name: Fetch genpolicy settings from fortress | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| run: | | |
| gh api "/repos/${FORTRESS_REPO}/contents/${FORTRESS_GENPOLICY_SETTINGS}?ref=${FORTRESS_REF}" \ | |
| --jq '.content' | base64 -d > cvm-artifacts/genpolicy-settings-base.json | |
| echo "Downloaded genpolicy settings from ${FORTRESS_REPO}@${FORTRESS_REF}" | |
| # ---------------------------------------------------------------- | |
| # Per-model loop: genpolicy -> initdata -> podspec -> cvm-measure | |
| # ---------------------------------------------------------------- | |
| - name: Generate policies and measurements | |
| run: | | |
| python3 scripts/generate-policies-and-measurements.py \ | |
| --models "${{ steps.models.outputs.list }}" \ | |
| --models-dir models/ \ | |
| --artifacts-dir artifacts/ \ | |
| --cvm-artifacts-dir cvm-artifacts/ \ | |
| --base-rules rules/rules.rego \ | |
| --base-settings cvm-artifacts/genpolicy-settings-base.json | |
| # ---------------------------------------------------------------- | |
| # Generate ITA policy (single policy, all models) | |
| # ---------------------------------------------------------------- | |
| - name: Generate ITA attestation policy | |
| run: | | |
| python3 scripts/generate-ita-policy.py \ | |
| --measurements-dir artifacts/ \ | |
| --template attestation-policy/template.rego \ | |
| --static-ref-vals attestation-policy/tdx-static-ref-vals.yaml \ | |
| --nonce "${{ github.run_id }}" \ | |
| --output artifacts/ita-attestation-policy.rego | |
| # ---------------------------------------------------------------- | |
| # Upload to ITA | |
| # ---------------------------------------------------------------- | |
| - name: Upload policy to ITA | |
| id: ita | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ITA_API_URL: ${{ secrets.ITA_API_URL }} | |
| ITA_ADMIN_API_KEY: ${{ secrets.ITA_ADMIN_API_KEY }} | |
| run: | | |
| # Sets ITA_POLICY_SLOT, ITA_POLICY_ID, ITA_POLICY_NAME | |
| if [[ "${{ inputs.ita_slot }}" != "auto" ]]; then | |
| eval "$(ITA_SLOT_OVERRIDE=${{ inputs.ita_slot }} ./scripts/select-ita-slot.sh)" | |
| else | |
| eval "$(./scripts/select-ita-slot.sh)" | |
| fi | |
| echo "ITA_POLICY_SLOT=$ITA_POLICY_SLOT" | |
| echo "ITA_POLICY_ID=$ITA_POLICY_ID" | |
| echo "ITA_POLICY_NAME=$ITA_POLICY_NAME" | |
| if [[ -n "$ITA_API_URL" && -n "$ITA_ADMIN_API_KEY" ]]; then | |
| ./scripts/upload-ita-policy.sh \ | |
| --policy-file artifacts/ita-attestation-policy.rego \ | |
| --policy-name "$ITA_POLICY_NAME" \ | |
| --policy-id "$ITA_POLICY_ID" \ | |
| --api-url "$ITA_API_URL" \ | |
| --api-key "$ITA_ADMIN_API_KEY" | |
| else | |
| echo "WARNING: ITA credentials not configured, skipping upload" | |
| fi | |
| echo "policy_id=$ITA_POLICY_ID" >> "$GITHUB_OUTPUT" | |
| echo "policy_slot=$ITA_POLICY_SLOT" >> "$GITHUB_OUTPUT" | |
| # ---------------------------------------------------------------- | |
| # Build predicates and sign with Sigstore (per model) | |
| # ---------------------------------------------------------------- | |
| - name: Fetch previous release log index | |
| id: chain | |
| run: | | |
| PREV_INDEX=0 | |
| LATEST=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "") | |
| if [[ -n "$LATEST" ]]; then | |
| echo "Previous release: $LATEST" | |
| # TODO: extract Rekor log index from previous release's attestation bundle | |
| # For now, use 0 (genesis) | |
| fi | |
| echo "previous_log_index=$PREV_INDEX" >> "$GITHUB_OUTPUT" | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Build predicates | |
| run: | | |
| python3 scripts/build-predicate.py \ | |
| --artifacts-dir artifacts/ \ | |
| --cvm-artifacts-dir cvm-artifacts/ \ | |
| --policy-id "${{ steps.ita.outputs.policy_id }}" \ | |
| --version "${{ inputs.version }}" \ | |
| --genpolicy-version "${{ env.GENPOLICY_VERSION }}" \ | |
| --cvm-measure-version "${{ env.CVM_MEASURE_VERSION }}" \ | |
| --previous-log-index "${{ steps.chain.outputs.previous_log_index }}" | |
| - name: Sign attestations with Sigstore (per model) | |
| run: | | |
| IFS=',' read -ra MODEL_ARRAY <<< "${{ steps.models.outputs.list }}" | |
| for MODEL in "${MODEL_ARRAY[@]}"; do | |
| MODEL=$(echo "$MODEL" | xargs) | |
| MEAS_FILE="artifacts/${MODEL}/measurements.json" | |
| PREDICATE_FILE="artifacts/${MODEL}/predicate.json" | |
| if [[ ! -f "$MEAS_FILE" || ! -f "$PREDICATE_FILE" ]]; then | |
| echo "Skipping $MODEL (missing artifacts)" | |
| continue | |
| fi | |
| echo "==========================================" | |
| echo "Signing attestation for: $MODEL" | |
| echo "==========================================" | |
| cosign attest-blob \ | |
| --predicate "$PREDICATE_FILE" \ | |
| --type "https://cohere.com/attestation-policy-ledger/v1" \ | |
| --bundle "artifacts/${MODEL}/attestation.sigstore.json" \ | |
| -y \ | |
| "$MEAS_FILE" | |
| echo "Bundle written: artifacts/${MODEL}/attestation.sigstore.json" | |
| done | |
| # ---------------------------------------------------------------- | |
| # Create GitHub Release | |
| # ---------------------------------------------------------------- | |
| - name: Prepare release assets | |
| id: assets | |
| run: | | |
| mkdir -p release-assets | |
| IFS=',' read -ra MODEL_ARRAY <<< "${{ steps.models.outputs.list }}" | |
| RELEASE_BODY="# Model Attestation Release ${{ inputs.version }}\n\n" | |
| if [[ -n "${{ inputs.reason }}" ]]; then | |
| RELEASE_BODY+="**Reason:** ${{ inputs.reason }}\n\n" | |
| fi | |
| RELEASE_BODY+="**ITA Policy ID:** \`${{ steps.ita.outputs.policy_id }}\`\n" | |
| RELEASE_BODY+="**ITA Policy Slot:** \`${{ steps.ita.outputs.policy_slot }}\`\n\n" | |
| RELEASE_BODY+="## Per-Model Artifacts\n\n" | |
| for MODEL in "${MODEL_ARRAY[@]}"; do | |
| MODEL=$(echo "$MODEL" | xargs) | |
| mkdir -p "release-assets/${MODEL}" | |
| for f in measurements.json policy.rego podspec_with_initdata.yaml predicate.json attestation.sigstore.json; do | |
| if [[ -f "artifacts/${MODEL}/${f}" ]]; then | |
| cp "artifacts/${MODEL}/${f}" "release-assets/${MODEL}/${f}" | |
| fi | |
| done | |
| MEAS=$(cat "artifacts/${MODEL}/measurements.json" 2>/dev/null || echo "{}") | |
| RELEASE_BODY+="### ${MODEL}\n\n" | |
| RELEASE_BODY+="\`\`\`json\n${MEAS}\n\`\`\`\n\n" | |
| done | |
| cp artifacts/ita-attestation-policy.rego release-assets/ | |
| echo -e "$RELEASE_BODY" > release-body.md | |
| - name: Create release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="${{ inputs.version }}" | |
| # Collect all asset files | |
| ASSET_FLAGS="" | |
| for f in $(find release-assets -type f); do | |
| ASSET_FLAGS="$ASSET_FLAGS $f" | |
| done | |
| gh release create "$TAG" \ | |
| --title "Model Attestation $TAG" \ | |
| --notes-file release-body.md \ | |
| $ASSET_FLAGS |