> ## Documentation Index
> Fetch the complete documentation index at: https://docs.futurex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure the Code Signing Pipeline

> Guide to set up a GitLab CI/CD pipeline that automates building and signing Windows executables.

In this section, we create a GitLab CI/CD pipeline that automatically builds Windows executables and submits them to CryptoHub for code signing. The pipeline uses a two-stage approach: **compile the executable with MinGW in the build stage**, then **submit it for signing and poll for approval in the sign stage**. This ensures signed artifacts are available for deployment without manual intervention.

## Configure GitLab CI/CD variables

Store sensitive credentials and connection details as protected CI/CD variables:

<Steps>
  <Step>
    Go to your GitLab project.
  </Step>

  <Step>
    Select **Settings** > **CI/CD**.
  </Step>

  <Step>
    Expand **Variables**.
  </Step>

  <Step>
    Add the following variables:

    <table>
      <thead>
        <tr>
          <th><strong>Variable name</strong></th>
          <th><strong>Value</strong></th>
        </tr>
      </thead>

      <tbody>
        <tr>
          <td><code>PKI\_P12\_B64</code></td>
          <td>Base64-encoded PKCS#12 certificate</td>
        </tr>

        <tr>
          <td><code>PKI\_PASS</code></td>
          <td>PKCS#12 certificate password</td>
        </tr>
      </tbody>
    </table>
  </Step>
</Steps>

**To encode your PKCS#12 certificate as Base64:**

```bash expandable lines wrap title="Bash" theme={null}
base64 -i pki.p12 | tr -d '\n' > pki.p12.b64
```

Copy the contents of `client-cert.p12.b64` into the `PKI_P12_B64` variable.

**Why mask and protect:** Protected variables are only exposed to protected branches (e.g., `main`). Masked variables are hidden in job logs. Both settings prevent credential exposure.

## Create the pipeline configuration file

In your project root directory, create or edit `.gitlab-ci.yml`:

```yaml expandable lines wrap title="YAML" theme={null}
stages:
  - build
  - sign

# ---- Build a Windows EXE with MinGW -----------------------------------------
build_exe:
  image: openturns/archlinux-mingw
  tags: [mingw]
  stage: build
  script:
    - x86_64-w64-mingw32-gcc main.c -o example.exe
  artifacts:
    paths:
      - example.exe

# ---- Submit for code signing + poll by request status -----------------------
sign_exe:
  image:
    name: docker-registry.futurex.com/futurex/fxcl/kmes-cli:1.9.2-2
  tags: [codesign]
  stage: sign
  timeout: 3d                 # also raise project-wide timeout in Settings → CI/CD → General pipelines
  variables:
    FXCL_SESSION: "./fxcl.kmes.session.$CI_JOB_ID"
    POLL_INTERVAL: "10"       # seconds between status checks
    MAX_ATTEMPTS: "0"         # 0 = infinite; else set e.g. 25920 for ~3 days at 10s
    CH_HOSTNAME: "integrations.uscentral1-cryptohub-uat.virtucrypt.com"
    CH_PORT: "2001"
  script:

    # Pull the client TLS certificate from the Gitlab instance project variables and save to encrypted file
    - mkdir -p .secure_files
    - echo "$PKI_P12_B64" | base64 -d > .secure_files/pki.p12
    - fxcli.kmes tls pki --file .secure_files/pki.p12 --password "$PKI_PASS"
    - fxcli.kmes connect -c "$CH_HOSTNAME":"$CH_PORT"

    # Submit request with unique name and capture REQUEST_ID
    - |
      set -eo pipefail
      DATETIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      REQ_NAME="Gitlab-Codesign-demo-$DATETIME"
      echo "Submitting signing request with name: ${REQ_NAME}"
      REQUEST_ID=$(fxcli.kmes code-sign-req submit \
        -a code-signing \
        -t sha256 \
        -f example.exe \
        --name "${REQ_NAME}" | yq -r '.request.id')
      echo "Submitted signing request. REQUEST_ID=${REQUEST_ID}"

    # Poll request STATUS via `code-sign-req get` (no --json flags)
    #   - if output indicates the request was deleted/unknown -> fail (exit 3)
    #   - denied  -> fail (exit 2)
    #   - pending -> sleep & retry
    #   - signed  -> download & finish
    - |
      set -eo pipefail
      ATTEMPTS=0
      while true; do
        ATTEMPTS=$((ATTEMPTS+1))

        # Capture stdout+stderr
        OUT_TEXT="$(fxcli.kmes code-sign-req get -i "$REQUEST_ID" 2>&1 || true)"

        # Deleted/unknown edge case detection
        if printf '%s' "$OUT_TEXT" | grep -qiE 'UNKNOWN PROFILE-BASED REQUEST|Failed to download request'; then
          echo "Request $REQUEST_ID no longer exists (deleted/unknown). Failing the job."
          printf '%s\n' "$OUT_TEXT"
          exit 3
        fi

        # Try to parse status with yq from YAML-like text
        STATUS="$(printf '%s' "$OUT_TEXT" \
          | yq -r '.request.status // .requests[0].status // ""' 2>/dev/null \
        )"

        # If yq path didn't work, fallback to regex "status: <value>"
        if [ -z "$STATUS" ] || [ "$STATUS" = "null" ]; then
          STATUS="$(printf '%s' "$OUT_TEXT" \
            | sed -n 's/^[[:space:]]*status:[[:space:]]*"\?\([^"]*\)"\?/\1/p' \
          )"

        echo "$(date -Iseconds) Request $REQUEST_ID status: $STATUS (attempt ${ATTEMPTS})"

        case "$STATUS" in
          denied)
            echo "Request was DENIED by approvers. Failing the job."
            exit 2
            ;;
          pending)
            if [ "$MAX_ATTEMPTS" != "0" ] && [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
              echo "Reached MAX_ATTEMPTS (${MAX_ATTEMPTS}) while still pending. Failing the job."
              exit 124
            fi
            sleep "${POLL_INTERVAL}"
            ;;
          signed)
            echo "Request is SIGNED. Downloading signed artifact..."
            if fxcli.kmes code-sign-req download -i "$REQUEST_ID" -f example.exe; then
              echo "Signed artifact downloaded."
              break
            else
              echo "Download failed unexpectedly after status=signed. Retrying in ${POLL_INTERVAL}s..."
              sleep "${POLL_INTERVAL}"
            fi
            ;;*)
            echo "Unexpected status '$STATUS'. Treating as transient; retrying in ${POLL_INTERVAL}s..."
            sleep "${POLL_INTERVAL}"
            ;;
        esac
      done

  artifacts:
    paths:
      - example.exe
    when: always
```

## Configure project-wide timeout settings

The sign job can run for up to 3 days while waiting for approval. Increase the project-wide timeout:

<Note>
  Why this matters: GitLab's default job timeout is 1 hour. Without this change, sign jobs will fail before approvers complete the workflow.
</Note>

<Steps>
  <Step>
    Go to **Settings** > **CI/CD**.
  </Step>

  <Step>
    Expand **General pipelines**.
  </Step>

  <Step>
    Set **Timeout** to `259200` seconds (3 days).
  </Step>

  <Step>
    Select **\[ Save changes ]**.
  </Step>
</Steps>

## Understand the pipeline structure

**Stages:**

* **build:** Compiles the Windows executable using MinGW cross-compiler
* **sign:** Submits the executable to CryptoHub, polls for approval, downloads the signed artifact

**Job routing:**

* `build_exe` job uses the `mingw` tag, routing it to runners configured for MinGW builds
* `sign_exe` job uses the `codesign` tag, routing it to runners with access to the Futurex registry

**Artifact passing:**

* The `build_exe` job produces `example.exe` as an artifact
* GitLab automatically makes this artifact available to the `sign_exe` job
* The `sign_exe` job overwrites `example.exe` with the signed version and publishes it as a new artifact

## Understand the build job configuration

```yaml expandable lines wrap title="YAML" theme={null}
build_exe:
  image: openturns/archlinux-mingw
  tags: [mingw]
  stage: build
  script:
    - x86_64-w64-mingw32-gcc main.c -o example.exe
  artifacts:
    paths:
      - example.exe
```

**Key elements:**

* `image`: Uses a public Docker image with MinGW cross-compiler pre-installed
* `tags: [mingw]`: Routes this job to runners with the `mingw` tag
* `script`: Compiles `main.c` into a Windows executable
* `artifacts`: Preserves `example.exe` for the sign stage

**Customization:** Replace `main.c` with your actual source files. For multi-file projects, adjust the compilation command or use a Makefile.

## Understand the sign job configuration

The sign job performs five operations:

1. **TLS setup:** Decodes the PKCS #12 certificate and configures CryptoHub connection
2. **Request submission:** Submits the executable for signing with a unique request name
3. **Status polling:** Polls request status every 10 seconds until it reaches a terminal state (signed, denied, deleted)
4. **Signature download:** Retrieves the signature for the executable when approval is complete. The Futurex CLI handles embedding the signature into the executable.

<Note>
  The GitLab service on the CryptoHub uses TLS authentication. This means authentication occurs automatically, immediately after the TLS negotiation, using the certificate itself rather than a username and password.
</Note>

**Configuration variables:**

<table>
  <thead>
    <tr>
      <th><strong>Variable</strong></th>
      <th><strong>Default</strong></th>
      <th><strong>Purpose</strong></th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>POLL\_INTERVAL</code></td>
      <td><code>10</code></td>
      <td>Seconds between status checks</td>
    </tr>

    <tr>
      <td><code>MAX\_ATTEMPTS</code></td>
      <td><code>0</code></td>
      <td>Maximum polling attempts (0 = infinite)</td>
    </tr>

    <tr>
      <td><code>CH\_HOSTNAME</code></td>
      <td>Must be defined. There is no default value.</td>
      <td>CryptoHub server hostname</td>
    </tr>

    <tr>
      <td><code>CH\_PORT</code></td>
      <td>Must be defined. There is no default value.</td>
      <td>CryptoHub server port</td>
    </tr>
  </tbody>
</table>

**Polling behavior:**

* **pending:** Job sleeps for `POLL_INTERVAL` seconds and checks status again
* **signed:** Job downloads the signature, and Futurex CLI embeds it in the executable.
* **denied:** Job fails immediately with exit code 2
* **deleted/unknown:** Job fails immediately with exit code 3 (request was removed from CryptoHub)
* **MAX\_ATTEMPTS reached:** Job fails with exit code 124 (timeout)

**Exit codes:**

<table>
  <thead>
    <tr>
      <th>Exit code</th>
      <th>Meaning</th>
      <th>Action required</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>0</td>
      <td>Success (signature downloaded)</td>
      <td>None</td>
    </tr>

    <tr>
      <td>2</td>
      <td>Request denied by approver</td>
      <td>Review denial reason in CryptoHub</td>
    </tr>

    <tr>
      <td>3</td>
      <td>Request deleted or unknown</td>
      <td>Check CryptoHub logs; request may have been manually removed</td>
    </tr>

    <tr>
      <td>124</td>
      <td>Polling timeout reached</td>
      <td>Increase <code>MAX\_ATTEMPTS</code> or resolve approval delays</td>
    </tr>
  </tbody>
</table>

## Commit and test the pipeline

<Steps>
  <Step>
    Commit `.gitlab-ci.yml` to your repository:

    ```Plaintext wrap theme={null}
    git add .gitlab-ci.yml
      git commit -m "Add CryptoHub code signing pipeline"
      git push origin main
    ```
  </Step>

  <Step>
    Go to your GitLab project and select **CI/CD** > **Pipelines**.
  </Step>

  <Step>
    Verify the pipeline starts automatically.
  </Step>

  <Step>
    Observe the build job complete within 1-2 minutes.
  </Step>

  <Step>
    Monitor the sign job logs. The job will display status checks every 10 seconds:

    ```Plaintext wrap theme={null}
    2025-01-15T14:32:10+00:00 Request 12345 status: pending (attempt 1)
      2025-01-15T14:32:20+00:00 Request 12345 status: pending (attempt 2)
    ```
  </Step>

  <Step>
    Approve the signing request in CryptoHub.
  </Step>

  <Step>
    Verify the sign job completes, downloads the signature, and embeds it into the artifact.
  </Step>
</Steps>

## Result

You now have a working CI/CD pipeline that:

* Automatically compiles Windows executables on every commit
* Submits signature requests to CryptoHub
* Polls for approval without manual intervention
* Downloads signatures from CryptoHub once they are approved and uses the Futurex CLI to embed them in artifacts for deployment

The pipeline enforces administrator approval, ensuring no executable is signed without proper authorization.
