# .github/workflows/examples/stellaops-sign.yml # StellaOps Keyless Sign Reusable Workflow # # This reusable workflow enables keyless signing of artifacts using Sigstore Fulcio. # It uses OIDC identity tokens from GitHub Actions to obtain ephemeral signing certificates. # # Usage: # jobs: # sign: # uses: stella-ops/templates/.github/workflows/stellaops-sign.yml@v1 # with: # artifact-digest: sha256:abc123... # artifact-type: image # permissions: # id-token: write # contents: read # # Prerequisites: # - StellaOps API accessible from runner # - OIDC token permissions granted # # See: docs/modules/signer/guides/keyless-signing.md name: StellaOps Keyless Sign on: workflow_call: inputs: artifact-digest: description: 'SHA256 digest of artifact to sign (e.g., sha256:abc123...)' required: true type: string artifact-type: description: 'Type of artifact: image, sbom, verdict, report' required: false type: string default: 'image' stellaops-url: description: 'StellaOps API URL' required: false type: string default: 'https://api.stella-ops.org' push-attestation: description: 'Push attestation to OCI registry' required: false type: boolean default: true predicate-type: description: 'Custom predicate type URI (optional)' required: false type: string default: '' include-rekor: description: 'Log signature to Rekor transparency log' required: false type: boolean default: true cli-version: description: 'StellaOps CLI version to use' required: false type: string default: 'latest' outputs: attestation-digest: description: 'Digest of created attestation' value: ${{ jobs.sign.outputs.attestation-digest }} rekor-uuid: description: 'Rekor transparency log UUID (if logged)' value: ${{ jobs.sign.outputs.rekor-uuid }} certificate-identity: description: 'OIDC identity bound to certificate' value: ${{ jobs.sign.outputs.certificate-identity }} signed-at: description: 'Signing timestamp (UTC ISO-8601)' value: ${{ jobs.sign.outputs.signed-at }} jobs: sign: runs-on: ubuntu-latest permissions: id-token: write # Required for OIDC token contents: read # Required for checkout packages: write # Required if pushing to GHCR outputs: attestation-digest: ${{ steps.sign.outputs.attestation-digest }} rekor-uuid: ${{ steps.sign.outputs.rekor-uuid }} certificate-identity: ${{ steps.sign.outputs.certificate-identity }} signed-at: ${{ steps.sign.outputs.signed-at }} steps: - name: Validate Inputs run: | if [[ ! "${{ inputs.artifact-digest }}" =~ ^sha256:[a-f0-9]{64}$ ]] && \ [[ ! "${{ inputs.artifact-digest }}" =~ ^sha512:[a-f0-9]{128}$ ]]; then echo "::error::Invalid artifact-digest format. Expected sha256:... or sha512:..." exit 1 fi VALID_TYPES="image sbom verdict report binary" if [[ ! " $VALID_TYPES " =~ " ${{ inputs.artifact-type }} " ]]; then echo "::error::Invalid artifact-type. Must be one of: $VALID_TYPES" exit 1 fi - name: Install StellaOps CLI uses: stella-ops/setup-cli@v1 with: version: ${{ inputs.cli-version }} - name: Get OIDC Token id: oidc run: | set -euo pipefail # Request OIDC token with sigstore audience OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \ -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | jq -r '.value') if [[ -z "$OIDC_TOKEN" || "$OIDC_TOKEN" == "null" ]]; then echo "::error::Failed to obtain OIDC token" exit 1 fi # Mask token in logs echo "::add-mask::${OIDC_TOKEN}" echo "token=${OIDC_TOKEN}" >> $GITHUB_OUTPUT # Extract identity for logging (non-sensitive) IDENTITY=$(echo "$OIDC_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq -r '.sub // "unknown"' 2>/dev/null || echo "unknown") echo "identity=${IDENTITY}" >> $GITHUB_OUTPUT - name: Keyless Sign id: sign env: STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }} STELLAOPS_URL: ${{ inputs.stellaops-url }} run: | set -euo pipefail SIGN_ARGS=( --keyless --artifact "${{ inputs.artifact-digest }}" --type "${{ inputs.artifact-type }}" --output json ) # Add optional predicate type if [[ -n "${{ inputs.predicate-type }}" ]]; then SIGN_ARGS+=(--predicate-type "${{ inputs.predicate-type }}") fi # Add Rekor logging option if [[ "${{ inputs.include-rekor }}" == "true" ]]; then SIGN_ARGS+=(--rekor) fi echo "::group::Signing artifact" RESULT=$(stella attest sign "${SIGN_ARGS[@]}") echo "$RESULT" | jq . echo "::endgroup::" # Extract outputs ATTESTATION_DIGEST=$(echo "$RESULT" | jq -r '.attestationDigest // empty') REKOR_UUID=$(echo "$RESULT" | jq -r '.rekorUuid // empty') CERT_IDENTITY=$(echo "$RESULT" | jq -r '.certificateIdentity // empty') SIGNED_AT=$(echo "$RESULT" | jq -r '.signedAt // empty') if [[ -z "$ATTESTATION_DIGEST" ]]; then echo "::error::Signing failed - no attestation digest returned" exit 1 fi echo "attestation-digest=${ATTESTATION_DIGEST}" >> $GITHUB_OUTPUT echo "rekor-uuid=${REKOR_UUID}" >> $GITHUB_OUTPUT echo "certificate-identity=${CERT_IDENTITY}" >> $GITHUB_OUTPUT echo "signed-at=${SIGNED_AT}" >> $GITHUB_OUTPUT - name: Push Attestation if: ${{ inputs.push-attestation }} env: STELLAOPS_URL: ${{ inputs.stellaops-url }} run: | set -euo pipefail echo "::group::Pushing attestation to registry" stella attest push \ --attestation "${{ steps.sign.outputs.attestation-digest }}" \ --registry "${{ github.repository }}" echo "::endgroup::" - name: Generate Summary run: | cat >> $GITHUB_STEP_SUMMARY << 'EOF' ## Attestation Created | Field | Value | |-------|-------| | **Artifact** | `${{ inputs.artifact-digest }}` | | **Type** | `${{ inputs.artifact-type }}` | | **Attestation** | `${{ steps.sign.outputs.attestation-digest }}` | | **Rekor UUID** | `${{ steps.sign.outputs.rekor-uuid || 'N/A' }}` | | **Certificate Identity** | `${{ steps.sign.outputs.certificate-identity }}` | | **Signed At** | `${{ steps.sign.outputs.signed-at }}` | | **Signing Mode** | Keyless (Fulcio) | ### Verification Command ```bash stella attest verify \ --artifact "${{ inputs.artifact-digest }}" \ --certificate-identity "${{ steps.sign.outputs.certificate-identity }}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ``` EOF