Skip to main content

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.

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:
1
Go to your GitLab project.
2
Select Settings > CI/CD.
3
Expand Variables.
4
Add the following variables:
Variable nameValue
PKIP12B64Base64-encoded PKCS#12 certificate
PKI_PASSPKCS#12 certificate password
To encode your PKCS#12 certificate as Base64:
Bash
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
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:
Why this matters: GitLab’s default job timeout is 1 hour. Without this change, sign jobs will fail before approvers complete the workflow.
1
Go to Settings > CI/CD.
2
Expand General pipelines.
3
Set Timeout to 259200 seconds (3 days).
4
Select [ Save changes ].

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
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.
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.
Configuration variables:
VariableDefaultPurpose
POLL_INTERVAL10Seconds between status checks
MAX_ATTEMPTS0Maximum polling attempts (0 = infinite)
CH_HOSTNAMEMust be defined. There is no default value.CryptoHub server hostname
CH_PORTMust be defined. There is no default value.CryptoHub server port
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:
Exit codeMeaningAction required
0Success (signature downloaded)None
2Request denied by approverReview denial reason in CryptoHub
3Request deleted or unknownCheck CryptoHub logs; request may have been manually removed
124Polling timeout reachedIncrease MAX_ATTEMPTS or resolve approval delays

Commit and test the pipeline

1
Commit .gitlab-ci.yml to your repository:
git add .gitlab-ci.yml
  git commit -m "Add CryptoHub code signing pipeline"
  git push origin main
2
Go to your GitLab project and select CI/CD > Pipelines.
3
Verify the pipeline starts automatically.
4
Observe the build job complete within 1-2 minutes.
5
Monitor the sign job logs. The job will display status checks every 10 seconds:
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)
6
Approve the signing request in CryptoHub.
7
Verify the sign job completes, downloads the signature, and embeds it into the artifact.

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.