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.
Store sensitive credentials and connection details as protected CI/CD variables:
Go to your GitLab project.
Add the following variables:| Variable name | Value |
|---|
PKI_P12_B64 | Base64-encoded PKCS#12 certificate |
PKI_PASS | PKCS#12 certificate password |
To encode your PKCS#12 certificate as Base64:
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:
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
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.
Expand General pipelines.
Set Timeout to 259200 seconds (3 days).
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
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:
- TLS setup: Decodes the PKCS #12 certificate and configures CryptoHub connection
- Request submission: Submits the executable for signing with a unique request name
- Status polling: Polls request status every 10 seconds until it reaches a terminal state (signed, denied, deleted)
- 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:
| Variable | Default | Purpose |
|---|
POLL_INTERVAL | 10 | Seconds between status checks |
MAX_ATTEMPTS | 0 | Maximum polling attempts (0 = infinite) |
CH_HOSTNAME | Must be defined. There is no default value. | CryptoHub server hostname |
CH_PORT | Must 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 code | Meaning | Action required |
|---|
| 0 | Success (signature downloaded) | None |
| 2 | Request denied by approver | Review denial reason in CryptoHub |
| 3 | Request deleted or unknown | Check CryptoHub logs; request may have been manually removed |
| 124 | Polling timeout reached | Increase MAX_ATTEMPTS or resolve approval delays |
Commit and test the pipeline
Commit .gitlab-ci.yml to your repository:git add .gitlab-ci.yml
git commit -m "Add CryptoHub code signing pipeline"
git push origin main
Go to your GitLab project and select CI/CD > Pipelines.
Verify the pipeline starts automatically.
Observe the build job complete within 1-2 minutes.
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)
Approve the signing request in CryptoHub.
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.