name: Generate and Upload Measurements

on:
  workflow_dispatch:
    inputs:
      osImage:
        description: "Full name of OS image (CSP independent image version UID)."
        type: string
        required: true
      isDebugImage:
        description: "Is OS image a debug image?"
        type: boolean
        required: true
      signMeasurements:
        description: "Sign and upload the measurements?"
        type: boolean
        required: true

jobs:
  calculate-measurements-on-csp:
    name: "Calculate Measurements on CSP"
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        provider: ["aws", "azure", "gcp"]
    permissions:
      id-token: write
      contents: read
    env:
      ARM_CLIENT_ID: ${{ secrets.AZURE_E2E_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.AZURE_E2E_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_E2E_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.AZURE_E2E_TENANT_ID }}
    outputs:
      ref: ${{ steps.extract.outputs.ref }}
      stream: ${{ steps.extract.outputs.stream }}
      version: ${{ steps.extract.outputs.version }}
    steps:
      - name: Check out repository
        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
        with:
          ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || '' }}

      - name: Extract ref, stream and version
        id: extract
        uses: ./.github/actions/shortname
        with:
          shortname: ${{ github.event.inputs.osImage }}

      - name: Check if image definition from build pipeline exists
        run: |
          wget -O /dev/null "https://cdn.confidential.cloud/constellation/v1/ref/${{ steps.extract.outputs.ref }}/stream/${{ steps.extract.outputs.stream }}/image/${{ steps.extract.outputs.version }}/info.json"
        shell: bash

      - name: Setup Go environment
        uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
        with:
          go-version: "1.19.4"

      - name: Build hack/pcr-reader
        run: |
          go build .
          pwd >> "$GITHUB_PATH"
        working-directory: hack/pcr-reader
        shell: bash

      - name: Login to Azure
        if: matrix.provider == 'azure'
        uses: ./.github/actions/login_azure
        with:
          azure_credentials: ${{ secrets.AZURE_E2E_CREDENTIALS }}

      - name: Create Azure resource group
        if: matrix.provider == 'azure'
        id: az_resource_group_gen
        shell: bash
        run: |
          uuid=$(cat /proc/sys/kernel/random/uuid)
          name=e2e-test-${uuid%%-*}
          az group create --location northeurope --name "$name" --tags e2e
          echo "res_group_name=$name" >> "$GITHUB_OUTPUT"

      - name: Create Cluster in E2E Test environment
        id: create_cluster
        uses: ./.github/actions/e2e_test
        with:
          workerNodesCount: 1
          controlNodesCount: 1
          cloudProvider: ${{ matrix.provider }}
          gcpProject: ${{ secrets.GCP_E2E_PROJECT }}
          gcp_service_account_json: ${{ secrets.GCP_SERVICE_ACCOUNT }}
          gcpClusterServiceAccountKey: ${{ secrets.GCP_CLUSTER_SERVICE_ACCOUNT }}
          azureSubscription: ${{ secrets.AZURE_E2E_SUBSCRIPTION_ID }}
          azureTenant: ${{ secrets.AZURE_E2E_TENANT_ID }}
          azureClientID: ${{ secrets.AZURE_E2E_CLIENT_ID }}
          azureClientSecret: ${{ secrets.AZURE_E2E_CLIENT_SECRET }}
          azureUserAssignedIdentity: ${{ secrets.AZURE_E2E_USER_ASSIGNED_IDENTITY }}
          azureResourceGroup: ${{ steps.az_resource_group_gen.outputs.res_group_name }}
          osImage: ${{ github.event.inputs.osImage }}
          isDebugImage: ${{ github.event.inputs.isDebugImage }}
          test: "nop"

      - name: Fetch PCRs from running cluster
        run: |
          KUBECONFIG="${PWD}/constellation-admin.conf" kubectl rollout status ds/verification-service -n kube-system --timeout=3m
          CONSTELL_IP=$(jq -r ".ip" constellation-id.json)
          mkdir -p "${{ github.workspace }}/generated-measurements"
          pcr-reader -constell-ip "${CONSTELL_IP}" -format json -metadata -csp "${{ matrix.provider }}" -image "${{ github.event.inputs.osImage }}" > "${{ github.workspace }}/generated-measurements/measurements.json"
          echo "All PCRs of current boot:"
          cat "${{ github.workspace }}/generated-measurements/measurements.json"
          case ${CSP} in
            aws)
              yq e 'del(.measurements.[1,10,16,17,18,19,20,21,22,23])' -i "${{ github.workspace }}/generated-measurements/measurements.json"
              yq '.measurements.4.warnOnly = false |
              .measurements.8.warnOnly = false |
              .measurements.9.warnOnly = false |
              .measurements.11.warnOnly = false |
              .measurements.12.warnOnly = false |
              .measurements.13.warnOnly = false |
              .measurements.15.warnOnly = false |
              .measurements.15.expected = "0000000000000000000000000000000000000000000000000000000000000000"' \
              -I 0 -o json -i "${{ github.workspace }}/generated-measurements/measurements.json"
              ;;
            azure)
            yq e 'del(.measurements.[0,6,10,16,17,18,19,20,21,22,23])' -I 0 -o json -i "${{ github.workspace }}/generated-measurements/measurements.json"
            yq '.measurements.4.warnOnly = false |
            .measurements.8.warnOnly = false |
            .measurements.9.warnOnly = false |
            .measurements.11.warnOnly = false |
            .measurements.12.warnOnly = false |
            .measurements.13.warnOnly = false |
            .measurements.15.warnOnly = false |
            .measurements.15.expected = "0000000000000000000000000000000000000000000000000000000000000000"' \
            -I 0 -o json -i "${{ github.workspace }}/generated-measurements/measurements.json"
            ;;
            gcp)
            yq e 'del(.measurements.[16,17,18,19,20,21,22,23])' -I 0 -o json -i "${{ github.workspace }}/generated-measurements/measurements.json"
            yq '.measurements.0.warnOnly = false |
            .measurements.4.warnOnly = false |
            .measurements.8.warnOnly = false |
            .measurements.9.warnOnly = false |
            .measurements.11.warnOnly = false |
            .measurements.12.warnOnly = false |
            .measurements.13.warnOnly = false |
            .measurements.15.warnOnly = false |
            .measurements.15.expected = "0000000000000000000000000000000000000000000000000000000000000000"' \
            -I 0 -o json -i "${{ github.workspace }}/generated-measurements/measurements.json"
            ;;
            *)
              echo "CSP case check failed!"
              exit 1
            ;;
          esac
          echo "PCRs to be published after removing known variable ones:"
          cat "${{ github.workspace }}/generated-measurements/measurements.json"
          mv "${{ github.workspace }}/generated-measurements/measurements.json" "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json"
        shell: bash
        env:
          CSP: ${{ matrix.provider }}

      - name: Upload measurements as artifact
        uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3.1.1
        with:
          name: measurements-${{ matrix.provider }}.json
          path: "${{ github.workspace }}/generated-measurements"

      - name: Always terminate cluster
        if: always()
        continue-on-error: true
        uses: ./.github/actions/constellation_destroy
        with:
          kubeconfig: ${{ steps.create_cluster.outputs.kubeconfig }}

      - name: Always destroy Azure resource group
        if: always() && matrix.provider == 'azure'
        shell: bash
        run: |
          az group delete \
            --name ${{ steps.az_resource_group_gen.outputs.res_group_name }} \
            --force-deletion-types Microsoft.Compute/virtualMachineScaleSets \
            --force-deletion-types Microsoft.Compute/virtualMachines \
            --no-wait \
            --yes

  validate-measurements:
    name: "Validate Measurements"
    needs: [calculate-measurements-on-csp]
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        provider: ["aws", "azure", "gcp"]
    steps:
      - name: Check out repository
        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
        with:
          ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || '' }}

      - name: Setup Go environment
        uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
        with:
          go-version: "1.19.4"

      - name: Build hack/pcr-compare
        run: |
          go build .
          pwd >> "$GITHUB_PATH"
        working-directory: hack/pcr-compare
        shell: bash

      - name: Download calculated measurements from artifact
        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3.1.1
        with:
          name: measurements-${{ matrix.provider }}.json
          path: "${{ github.workspace }}/generated-measurements"

      - name: Download expected measurements from build pipeline for image
        run: |
          path="constellation/v1/ref/${ref}/stream/${stream}/image/${version}/csp/${{ matrix.provider }}/measurements.image.json"
          mkdir -p ${{ github.workspace }}/expected-measurements
          wget -O ${{ github.workspace }}/expected-measurements/measurements.image.json "https://cdn.confidential.cloud/${path}"
          cat ${{ github.workspace }}/expected-measurements/measurements.image.json
        shell: bash
        env:
          ref: ${{ needs.calculate-measurements-on-csp.outputs.ref }}
          stream: ${{ needs.calculate-measurements-on-csp.outputs.stream }}
          version: ${{ needs.calculate-measurements-on-csp.outputs.version }}

      - name: Check if expected measurements == actual measurements from running cluster
        run: |
          pcr-compare ${{ github.workspace }}/expected-measurements/measurements.image.json ${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json
        shell: bash

  sign-measurements:
    name: "Sign Measurements"
    if: inputs.signMeasurements
    needs: [calculate-measurements-on-csp, validate-measurements]
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        provider: ["aws", "azure", "gcp"]
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Install Cosign
        uses: sigstore/cosign-installer@9becc617647dfa20ae7b1151972e9b3a2c338a2b # tag=v2.8.1

      - name: Install Rekor
        shell: bash
        run: |
          curl -sLO https://github.com/sigstore/rekor/releases/download/v0.12.0/rekor-cli-linux-amd64
          sudo install rekor-cli-linux-amd64 /usr/local/bin/rekor-cli
          rm rekor-cli-linux-amd64

      - name: Download measurements from artifact
        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3.0.1
        with:
          name: measurements-${{ matrix.provider }}.json
          path: "${{ github.workspace }}/generated-measurements"

      - name: Sign measurements
        shell: bash
        env:
          COSIGN_PUBLIC_KEY: ${{ startsWith(github.ref, 'refs/heads/release/v') && secrets.COSIGN_PUBLIC_KEY || secrets.COSIGN_DEV_PUBLIC_KEY }}
          COSIGN_PRIVATE_KEY: ${{ startsWith(github.ref, 'refs/heads/release/v') && secrets.COSIGN_PRIVATE_KEY || secrets.COSIGN_DEV_PRIVATE_KEY }}
          COSIGN_PASSWORD: ${{ startsWith(github.ref, 'refs/heads/release/v') && secrets.COSIGN_PASSWORD || secrets.COSIGN_DEV_PASSWORD }}
        run: |
          echo "${COSIGN_PUBLIC_KEY}" > cosign.pub
          # Enabling experimental mode also publishes signature to Rekor
          COSIGN_EXPERIMENTAL=1 cosign sign-blob --key env://COSIGN_PRIVATE_KEY "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json" > "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json.sig"
          # Verify - As documentation & check
          # Local Signature (input: artifact, key, signature)
          cosign verify-blob --key cosign.pub --signature "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json.sig" "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json"
          # Transparency Log Signature (input: artifact, key)
          uuid=$(rekor-cli search --artifact "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json" | tail -n 1)
          sig=$(rekor-cli get --uuid="${uuid}" --format=json | jq -r .Body.HashedRekordObj.signature.content)
          cosign verify-blob --key cosign.pub --signature <(echo "${sig}") "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json"

      - name: Upload signed measurements as artifact
        uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3.1.1
        with:
          name: measurements-${{ matrix.provider }}.json.sig
          path: "${{ github.workspace }}/generated-measurements"

  publish-measurements:
    name: "Publish Measurements"
    if: inputs.signMeasurements
    needs:
      [calculate-measurements-on-csp, validate-measurements, sign-measurements]
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        provider: ["aws", "azure", "gcp"]
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Download measurements from artifact
        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3.0.1
        with:
          name: measurements-${{ matrix.provider }}.json
          path: "${{ github.workspace }}/generated-measurements"

      - name: Download signature from artifact
        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3.0.1
        with:
          name: measurements-${{ matrix.provider }}.json.sig
          path: "${{ github.workspace }}/generated-measurements"

      - name: Login to AWS
        uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # tag=v1.7.0
        with:
          role-to-assume: arn:aws:iam::795746500882:role/GitHubConstellationImagePipeline
          aws-region: eu-central-1

      - name: Upload to S3
        run: |
          S3_PATH=s3://cdn-constellation-backend/constellation/v1/ref/${ref}/stream/${stream}/image/${version}/csp/${{ matrix.provider }}
          aws s3 cp "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json" "${S3_PATH}/measurements.json"
          aws s3 cp "${{ github.workspace }}/generated-measurements/measurements-${{ matrix.provider }}.json.sig" "${S3_PATH}/measurements.json.sig"
        shell: bash
        env:
          IMAGE_UID: ${{ inputs.osImage }}
          PUBLIC_BUCKET_NAME: ${{ secrets.PUBLIC_BUCKET_NAME }}
          CSP: ${{ matrix.provider }}
          ref: ${{ needs.calculate-measurements-on-csp.outputs.ref }}
          stream: ${{ needs.calculate-measurements-on-csp.outputs.stream }}
          version: ${{ needs.calculate-measurements-on-csp.outputs.version }}
