Automated SIGN+ Package Installation and Configuration

Configure your GitHub Actions workflow to install and configure SIGN+ automatically on every run, without manual runner setup.

Overview

Previously, you had to manually install and configure the SIGN+ package on each runner before any signing job could run. This manual setup created extra work whenever you added new runners, scaled your infrastructure, or moved workflows to a new environment.

Starting with version 2026.3.0, you can configure your GitHub Actions workflow to install and configure SIGN+ automatically every time it runs without touching the runner manually. The workflow detects whether SIGN+ is already installed; if it is, installation is skipped and the signing step starts immediately.

This topic covers three approaches for getting the SIGN+ package onto your runner. Choose the approach that best fits your team's setup.

Note: The examples in this topic demonstrate signing a Java JAR file with jarsigner on a Linux self-hosted runner. Use the generated JSON configuration file (described in Understanding the JSON Configuration File) to retrieve the correct commands for other file types such as Android APK, Windows PE, XML, or container images.

Prerequisites

Make sure you have the following before you set up any of the three approaches:

  • An active AppViewX account with a configured code-signing policy.
  • Your OAuth client ID and client secret (or username and password for basic authentication). Store these as encrypted Secrets in your GitHub repository never put credentials directly in workflow files.
  • The name of your SIGN+ signing policy. Your AppViewX administrator can provide this.
  • A self-hosted Linux runner with jq, unzip, Java, and Maven available (or permission to install them during the workflow run).

Where to store secrets in GitHub

In your GitHub repository, go to Settings > Security and quality > Secrets and variables > Actions. Add your credentials under the Secrets tab so their values are always masked in workflow logs.

How automated installation works

The automated installation follows the same steps every time:

  1. Get the SIGN+ package. The workflow either reads the package from the repository, downloads it from the AppViewX API, or downloads it using a secure OAuth token.
  2. Check whether SIGN+ is already installed. If the package is already installed at the configured work directory, the workflow skips installation. This saves time on repeated runs.
  3. Install SIGN+. If this is the first run, the workflow extracts the package, sets the correct permissions, and runs the SIGN+ Installer.
  4. Read the signing commands. SIGN+ generates a JSON configuration file with the exact signing and verification commands for your policy. The workflow reads this file.
  5. Sign and verify your artifact. The workflow runs the signing command, then verifies the signature.

Approach 1: Store the SIGN+ package in your repository (Recommended)

In this approach, you commit the SIGN+ package file directly to your Git repository. The workflow reads it from there no internet connection to the AppViewX server is needed at workflow run time.

This is the recommended approach for most teams because it:

  • Keeps workflow run times short no large file download on every build.
  • Removes the runtime dependency on the AppViewX server being reachable.
  • Works reliably even during network disruptions.
Tip: If your organization does not allow large binary files in Git, store the SIGN+ package in a secure artifact storage service (such as GitHub Packages or Amazon S3) and update the SIGN_PLUS_ZIP variable to point to that location.

Repository structure

your-repository/
├── .github/
│   └── workflows/
│       └── main_ci-sign.yml               ← workflow file
├── scripts/
│   └── sign-with-signplus.sh              ← signing script
├── src/
├── pom.xml
└── <YourPolicyName>_SIGN+_Package.zip    ← SIGN+ package committed here

Step-by-step setup

  1. Download the SIGN+ package for Linux from the AppViewX portal.
  2. Commit the downloaded .zip file to the root of your repository.
  3. Create scripts/sign-with-signplus.sh using the content in The Signing Script.
  4. Create .github/workflows/main_ci-sign.yml using the workflow YAML below. Replace <YourPolicyName>_SIGN+_Package.zip with your actual zip file name, and update SIGNPLUS_POLICY in the signing script to match your policy name.
  5. Add two repository secrets in GitHub at Settings > Security and quality > Secrets and variables > Actions:
    • SIGNPLUS_CLIENTID your OAuth client ID.
    • SIGNPLUS_CLIENTSECRET your OAuth client secret.
  6. Push to main or master to start a workflow run.

Workflow file .github/workflows/main_ci-sign.yml

# GitHub Secrets required (Settings → Security and quality → Secrets and variables → Actions):
#   SIGNPLUS_CLIENTID      OAuth client ID
#   SIGNPLUS_CLIENTSECRET  OAuth client secret
#
# Runner: self-hosted Linux. Requires Java, Maven, jq, unzip.

name: CI Sign (OAuth zip)

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]
  workflow_dispatch:

jobs:
  build:
    name: Build Java
    runs-on: Linux
    steps:
      - name: Checkout source
        uses: actions/checkout@v4

      - name: Verify Java and Maven
        run: |
          set -e
          java -version
          mvn -version

      - name: Maven clean test package
        run: mvn -B clean test package

      - name: Upload unsigned JAR
        uses: actions/upload-artifact@v4
        with:
          name: unsigned-jar
          path: target/simple-poc-1.0.0.jar
          if-no-files-found: error

  sign:
    name: Sign JAR with SIGN+
    runs-on: Linux
    needs: build
    steps:
      - name: Checkout scripts and SIGN+ zip
        uses: actions/checkout@v4

      - name: Download unsigned JAR
        uses: actions/download-artifact@v4
        with:
          name: unsigned-jar
          path: ${{ runner.temp }}/unsigned-jar

      - name: Ensure jq and unzip
        run: |
          set -e
          if ! command -v jq >/dev/null 2>&1; then
            sudo apt-get update -qq
            sudo apt-get install -y -qq jq unzip
          fi
          jq --version

      - name: SIGN+ install, sign, verify
        env:
          SIGNPLUS_CLIENTID: ${{ secrets.SIGNPLUS_CLIENTID }}
          SIGNPLUS_CLIENTSECRET: ${{ secrets.SIGNPLUS_CLIENTSECRET }}
          SIGN_PLUS_ZIP: ${{ github.workspace }}/<YourPolicyName>_SIGN+_Package.zip
          INPUT_JAR: ${{ runner.temp }}/unsigned-jar/simple-poc-1.0.0.jar
          OUTPUT_JAR: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          SIGN_PLUS_WORK_DIR: ${{ runner.temp }}/signplus
        run: |
          set -euo pipefail
          chmod +x scripts/sign-with-signplus.sh
          mkdir -p "${{ runner.temp }}/signed"
          scripts/sign-with-signplus.sh

      - name: Upload signed JAR
        uses: actions/upload-artifact@v4
        with:
          name: signed-jar
          path: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          if-no-files-found: error

The signing script

Create scripts/sign-with-signplus.sh in your repository. This script installs SIGN+ (if not already installed) and runs the signing and verification commands.

Required environment variables

Variable Description
SIGNPLUS_CLIENTID Your OAuth client ID from AppViewX.
SIGNPLUS_CLIENTSECRET Your OAuth client secret. Store as a GitHub repository secret.
SIGN_PLUS_ZIP Full path to the SIGN+ package .zip file.
INPUT_JAR Full path to the unsigned artifact to sign.
OUTPUT_JAR Full path where the signed artifact is saved.
SIGN_PLUS_WORK_DIR (Optional) Directory where SIGN+ is installed and cached between runs. Defaults to ~/signplus-package.
#!/usr/bin/env bash
# Installs SIGN+ (idempotent) and signs/verifies a single JAR using jarsigner.
set -euo pipefail

if [[ -z "${SIGNPLUS_CLIENTID:-}" || -z "${SIGNPLUS_CLIENTSECRET:-}" ]]; then
  echo "[ERROR] SIGNPLUS_CLIENTID and/or SIGNPLUS_CLIENTSECRET is empty."
  exit 1
fi

: "${SIGNPLUS_CLIENTID:?SIGNPLUS_CLIENTID is required}"
: "${SIGNPLUS_CLIENTSECRET:?SIGNPLUS_CLIENTSECRET is required}"
: "${SIGN_PLUS_ZIP:?SIGN_PLUS_ZIP is required}"
: "${INPUT_JAR:?INPUT_JAR is required}"
: "${OUTPUT_JAR:?OUTPUT_JAR is required}"

if [[ ! -f "$SIGN_PLUS_ZIP" ]]; then
  echo "[ERROR] SIGN+ zip not found: $SIGN_PLUS_ZIP"; exit 1
fi
if [[ ! -f "$INPUT_JAR" ]]; then
  echo "[ERROR] Input JAR not found: $INPUT_JAR"; exit 1
fi

SIGNPLUS_AUTHTYPE="oAuth"
SIGNPLUS_POLICY="AzurePolicy"   # Update to match your policy name

WORK_DIR="${SIGN_PLUS_WORK_DIR:-$HOME/signplus-package}"
SIGN_PLUS_PACKAGE_DIR="$WORK_DIR/$(basename "$SIGN_PLUS_ZIP" .zip)"
mkdir -p "$WORK_DIR"

# Install SIGN+ (skipped if already installed)
if [[ -f "$SIGN_PLUS_PACKAGE_DIR/SIGN+_Installer" ]]; then
  echo "[INFO] SIGN+ already installed — skipping."
else
  echo "[STEP 1] Extracting SIGN+ package..."
  unzip -o "$SIGN_PLUS_ZIP" -d "$SIGN_PLUS_PACKAGE_DIR"

  echo "[STEP 2] Setting installer permissions..."
  chmod +x "$SIGN_PLUS_PACKAGE_DIR/SIGN+_Installer"

  echo "[STEP 3] Installing SIGN+..."
  cd "$SIGN_PLUS_PACKAGE_DIR"
  ./SIGN+_Installer Install \
    --authtype "$SIGNPLUS_AUTHTYPE" \
    --username "$SIGNPLUS_CLIENTID" \
    --password "$SIGNPLUS_CLIENTSECRET" \
    --overwriteInstallation
fi

cd "$SIGN_PLUS_PACKAGE_DIR"
echo "[STEP 4] Current SIGN+ configuration:"
./SIGN+_Installer Print

JSON_FILE="$SIGN_PLUS_PACKAGE_DIR/README_${SIGNPLUS_POLICY}.json"
if [[ ! -f "$JSON_FILE" ]]; then
  echo "[ERROR] JSON config not found: $JSON_FILE"
  find "$SIGN_PLUS_PACKAGE_DIR" -maxdepth 2 -type f -print
  exit 1
fi

echo "[STEP 5] Reading signing commands from $JSON_FILE..."
RAW_SIGN_CMD=$(jq -r '.pkcs11.sign.jarsigner' "$JSON_FILE")
RAW_VERIFY_CMD=$(jq -r '.pkcs11.verify.jarsigner' "$JSON_FILE")

if [[ "$RAW_SIGN_CMD" == "null" || -z "$RAW_SIGN_CMD" ]]; then
  echo "[ERROR] jarsigner sign command not found in JSON"; exit 1
fi

mkdir -p "$(dirname "$OUTPUT_JAR")"

FINAL_SIGN_CMD="${RAW_SIGN_CMD//<input_file_path>/$INPUT_JAR}"
FINAL_SIGN_CMD="${FINAL_SIGN_CMD//<output_file_path>/$OUTPUT_JAR}"
FINAL_VERIFY_CMD="${RAW_VERIFY_CMD//<signed_file_path>/$OUTPUT_JAR}"

echo "[STEP 6] Signing artifact..."
eval "$FINAL_SIGN_CMD"

echo "[STEP 7] Verifying signature..."
eval "$FINAL_VERIFY_CMD"

if [[ ! -f "$OUTPUT_JAR" ]]; then
  echo "[ERROR] Signed artifact was not created: $OUTPUT_JAR"; exit 1
fi
echo "Signed artifact ready: $OUTPUT_JAR"

Approach 2: Download the SIGN+ package at runtime

CAUTION: The SIGN+ package is a large file. Downloading it from the AppViewX server on every workflow run adds time to each build and creates a dependency on the AppViewX server being reachable. Use this approach only when storing the package in the repository or on the runner is not possible.

The download script caches the package after the first download. Subsequent runs on the same runner reuse the cached file.

Two variants are available:

  • Variant A Basic download: Downloads the default SIGN+ package for your operating system. Use this when your signing policy does not require a specific certificate thumbprint at download time.
  • Variant B Policy-specific download: Downloads a package pre-configured for a specific signing policy and certificate. Use this when you need to target a specific policy in AppViewX.

Repository structure for Approach 2

your-repository/
├── .github/
│   └── workflows/
│       └── ci-sign.yml
├── scripts/
│   ├── download-signplus-api.sh   ← downloads the SIGN+ package
│   └── sign-with-signplus.sh      ← installs SIGN+ and signs the artifact
├── src/
└── pom.xml

Variant A Basic package download workflow (basic-package-curl_ci-sign.yml)

# GitHub Secrets required:
#   SIGNPLUS_USERNAME
#   SIGNPLUS_PASSWORD
#
# Runner: self-hosted Linux. Requires Java, Maven, jq, unzip, curl.
# Network access to SIGNPLUS_API_HOST is required.

name: CI Sign (basic API package)

on:
  push:
    branches: [main, master]
  workflow_dispatch:

env:
  SIGNPLUS_API_HOST: "<your-appviewx-server>"
  SIGNPLUS_API_PORT: "<api-port>"
  SIGNPLUS_PACKAGE_TYPE: Linux
  SIGNPLUS_AUTH_TYPE: "oAuth"
  SIGNPLUS_POLICY_NAME: "<your-policy-name>"

jobs:
  build:
    name: Build Java
    runs-on: Linux
    steps:
      - name: Checkout source
        uses: actions/checkout@v4
      - name: Maven clean test package
        run: mvn -B clean test package
      - name: Upload unsigned JAR
        uses: actions/upload-artifact@v4
        with:
          name: unsigned-jar
          path: target/simple-poc-1.0.0.jar
          if-no-files-found: error

  sign:
    name: Sign JAR (API download)
    runs-on: Linux
    needs: build
    steps:
      - name: Checkout scripts
        uses: actions/checkout@v4

      - name: Download unsigned JAR
        uses: actions/download-artifact@v4
        with:
          name: unsigned-jar
          path: ${{ runner.temp }}/unsigned-jar

      - name: Ensure jq, unzip, curl
        run: |
          set -e
          if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
            sudo apt-get update -qq
            sudo apt-get install -y -qq jq unzip curl
          fi

      - name: Download SIGN+ package via API
        env:
          SIGNPLUS_USERNAME: ${{ secrets.SIGNPLUS_USERNAME }}
          SIGNPLUS_PASSWORD: ${{ secrets.SIGNPLUS_PASSWORD }}
          SIGNPLUS_API_HOST: ${{ env.SIGNPLUS_API_HOST }}
          SIGNPLUS_API_PORT: ${{ env.SIGNPLUS_API_PORT }}
          SIGNPLUS_PACKAGE_TYPE: ${{ env.SIGNPLUS_PACKAGE_TYPE }}
          OUTPUT_FILE: ${{ runner.temp }}/signplus_package.zip
        run: |
          set -euo pipefail
          chmod +x scripts/download-signplus-api.sh
          scripts/download-signplus-api.sh
          echo "SIGN_PLUS_ZIP=${OUTPUT_FILE}" >> "$GITHUB_ENV"

      - name: Install SIGN+, sign and verify
        env:
          SIGNPLUS_CLIENTID: ${{ secrets.SIGNPLUS_USERNAME }}
          SIGNPLUS_CLIENTSECRET: ${{ secrets.SIGNPLUS_PASSWORD }}
          SIGNPLUS_POLICY_NAME: ${{ env.SIGNPLUS_POLICY_NAME }}
          INPUT_JAR: ${{ runner.temp }}/unsigned-jar/simple-poc-1.0.0.jar
          OUTPUT_JAR: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          SIGN_PLUS_WORK_DIR: ${{ runner.temp }}/signplus
        run: |
          set -euo pipefail
          mkdir -p "${{ runner.temp }}/signed"
          chmod +x scripts/sign-with-signplus.sh
          scripts/sign-with-signplus.sh

      - name: Upload signed JAR
        uses: actions/upload-artifact@v4
        with:
          name: signed-jar
          path: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          if-no-files-found: error

Variant A Download script (scripts/download-signplus-api.sh)

#!/usr/bin/env bash
# Downloads the SIGN+ package from the AppViewX API (basic, no policy filter).
set -euo pipefail

: "${SIGNPLUS_USERNAME:?SIGNPLUS_USERNAME is required}"
: "${SIGNPLUS_PASSWORD:?SIGNPLUS_PASSWORD is required}"
: "${SIGNPLUS_API_HOST:?SIGNPLUS_API_HOST is required}"
: "${SIGNPLUS_API_PORT:?SIGNPLUS_API_PORT is required}"
: "${OUTPUT_FILE:?OUTPUT_FILE is required}"

PACKAGE_TYPE="${SIGNPLUS_PACKAGE_TYPE:-Linux}"
API_URL="https://${SIGNPLUS_API_HOST}:${SIGNPLUS_API_PORT}/avxapi/code-signing-download-csp-pkcs11-dll-sign?gwsource=external"

# Skip if package is already cached and valid
if [[ -f "$OUTPUT_FILE" ]] && unzip -t "$OUTPUT_FILE" >/dev/null 2>&1; then
  echo "[INFO] SIGN+ package already cached — skipping download."
  exit 0
fi

mkdir -p "$(dirname "$OUTPUT_FILE")"

JSON_PAYLOAD=$(cat <<EOF
{
    "payload": {
        "packageType": "${PACKAGE_TYPE}"
    }
}
EOF
)

TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT

HTTP_STATUS=$(curl -k --location "${API_URL}" \
  --header "Content-Type: application/json" \
  --header "username: ${SIGNPLUS_USERNAME}" \
  --header "password: ${SIGNPLUS_PASSWORD}" \
  --data "${JSON_PAYLOAD}" \
  --output "${TMPFILE}" \
  -w '%{http_code}')

if [[ "${HTTP_STATUS}" -eq 200 ]]; then
  FILE_CONTENT=$(jq -r '.response.fileContent // empty' "${TMPFILE}")
  if [[ -z "${FILE_CONTENT}" ]]; then
    echo "[ERROR] Response did not contain .response.fileContent"
    exit 1
  fi
  echo "${FILE_CONTENT}" | base64 -d > "${OUTPUT_FILE}"
  unzip -t "${OUTPUT_FILE}" >/dev/null
  echo "[INFO] Package downloaded and verified: ${OUTPUT_FILE}"
else
  echo "[ERROR] HTTP ${HTTP_STATUS} — download failed."
  exit 1
fi

Variant B Policy-specific download workflow (signplus-zip-curl_ci-sign.yml)

Use Variant B when you need a package pre-configured for a specific signing policy and certificate thumbprint. Set SIGNPLUS_POLICY_VALUE as a repository variable in GitHub at Settings > Secrets and variables > Actions > Variables tab.

# GitHub Secrets required:
#   SIGNPLUS_USERNAME
#   SIGNPLUS_PASSWORD
#
# GitHub Variables (Settings → Secrets and variables → Actions → Variables tab):
#   SIGNPLUS_POLICY_VALUE  (optional — overrides the default below)

name: CI Sign (policy API package)

on:
  push:
    branches: [main, master]
  workflow_dispatch:

env:
  SIGNPLUS_API_HOST: "<your-appviewx-server>"
  SIGNPLUS_API_PORT: "<api-port>"
  SIGNPLUS_PAYLOAD_USER_NAME: "<appviewx-username>"
  SIGNPLUS_PACKAGE_TYPE: Linux
  SIGNPLUS_AUTH_TYPE: basicAuth
  SIGNPLUS_POLICY_NAME: "<your-policy-name>"
  SIGNPLUS_POLICY_VALUE_DEFAULT: "<commonName>=<serialNumber>"

jobs:
  build:
    name: Build Java
    runs-on: Linux
    steps:
      - name: Checkout source
        uses: actions/checkout@v4
      - name: Maven clean test package
        run: mvn -B clean test package
      - name: Upload unsigned JAR
        uses: actions/upload-artifact@v4
        with:
          name: unsigned-jar
          path: target/simple-poc-1.0.0.jar
          if-no-files-found: error

  sign:
    name: Sign JAR (policy API package)
    runs-on: Linux
    needs: build
    steps:
      - name: Checkout scripts
        uses: actions/checkout@v4

      - name: Download unsigned JAR
        uses: actions/download-artifact@v4
        with:
          name: unsigned-jar
          path: ${{ runner.temp }}/unsigned-jar

      - name: Ensure jq, unzip, curl
        run: |
          set -e
          if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
            sudo apt-get update -qq
            sudo apt-get install -y -qq jq unzip curl
          fi

      - name: Download SIGN+ package via API
        env:
          SIGNPLUS_USERNAME: ${{ secrets.SIGNPLUS_USERNAME }}
          SIGNPLUS_PASSWORD: ${{ secrets.SIGNPLUS_PASSWORD }}
          SIGNPLUS_API_HOST: ${{ env.SIGNPLUS_API_HOST }}
          SIGNPLUS_API_PORT: ${{ env.SIGNPLUS_API_PORT }}
          SIGNPLUS_PAYLOAD_USER_NAME: ${{ env.SIGNPLUS_PAYLOAD_USER_NAME }}
          SIGNPLUS_PACKAGE_TYPE: ${{ env.SIGNPLUS_PACKAGE_TYPE }}
          SIGNPLUS_AUTH_TYPE: ${{ env.SIGNPLUS_AUTH_TYPE }}
          SIGNPLUS_POLICY_NAME: ${{ env.SIGNPLUS_POLICY_NAME }}
          SIGNPLUS_POLICY_VALUE: ${{ vars.SIGNPLUS_POLICY_VALUE || env.SIGNPLUS_POLICY_VALUE_DEFAULT }}
          OUTPUT_FILE: ${{ runner.temp }}/signplus_package.zip
        run: |
          set -euo pipefail
          chmod +x scripts/download-signplus-api.sh
          scripts/download-signplus-api.sh
          echo "SIGN_PLUS_ZIP=${OUTPUT_FILE}" >> "$GITHUB_ENV"

      - name: Install SIGN+, sign and verify
        env:
          SIGNPLUS_CLIENTID: ${{ secrets.SIGNPLUS_USERNAME }}
          SIGNPLUS_CLIENTSECRET: ${{ secrets.SIGNPLUS_PASSWORD }}
          SIGNPLUS_POLICY_NAME: ${{ env.SIGNPLUS_POLICY_NAME }}
          INPUT_JAR: ${{ runner.temp }}/unsigned-jar/simple-poc-1.0.0.jar
          OUTPUT_JAR: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          SIGN_PLUS_WORK_DIR: ${{ runner.temp }}/signplus
        run: |
          set -euo pipefail
          mkdir -p "${{ runner.temp }}/signed"
          chmod +x scripts/sign-with-signplus.sh
          scripts/sign-with-signplus.sh

      - name: Upload signed JAR
        uses: actions/upload-artifact@v4
        with:
          name: signed-jar
          path: ${{ runner.temp }}/signed/simple-poc-1.0.0_signed.jar
          if-no-files-found: error

Variant B Policy-specific download script (scripts/download-signplus-api.sh)

#!/usr/bin/env bash
# Downloads the SIGN+ package for a specific signing policy.
set -euo pipefail

: "${SIGNPLUS_USERNAME:?}"
: "${SIGNPLUS_PASSWORD:?}"
: "${SIGNPLUS_API_HOST:?}"
: "${SIGNPLUS_API_PORT:?}"
: "${SIGNPLUS_POLICY_NAME:?}"
: "${SIGNPLUS_POLICY_VALUE:?}"
: "${OUTPUT_FILE:?}"

PAYLOAD_USER_NAME="${SIGNPLUS_PAYLOAD_USER_NAME:-$SIGNPLUS_USERNAME}"
PACKAGE_TYPE="${SIGNPLUS_PACKAGE_TYPE:-Linux}"
AUTH_TYPE="${SIGNPLUS_AUTH_TYPE:-basicAuth}"
API_URL="https://${SIGNPLUS_API_HOST}:${SIGNPLUS_API_PORT}/avxapi/code-signing-download-csp-pkcs11-dll-sign?gwsource=external"

if [[ -f "$OUTPUT_FILE" ]] && unzip -t "$OUTPUT_FILE" >/dev/null 2>&1; then
  echo "[INFO] Package cached at $OUTPUT_FILE — skipping download."
  exit 0
fi

mkdir -p "$(dirname "$OUTPUT_FILE")"

JSON_PAYLOAD=$(cat <<EOF
{
    "payload": {
        "userName": "${PAYLOAD_USER_NAME}",
        "packageType": "${PACKAGE_TYPE}",
        "authType": "${AUTH_TYPE}",
        "signingPolicy": {
            "${SIGNPLUS_POLICY_NAME}": "${SIGNPLUS_POLICY_VALUE}"
        }
    }
}
EOF
)

TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT

HTTP_STATUS=$(curl -k --location "${API_URL}" \
  --header "Content-Type: application/json" \
  --header "username: ${SIGNPLUS_USERNAME}" \
  --header "password: ${SIGNPLUS_PASSWORD}" \
  --data "${JSON_PAYLOAD}" \
  --output "${TMPFILE}" \
  -w '%{http_code}')

if [[ "${HTTP_STATUS}" -eq 200 ]]; then
  FILE_CONTENT=$(jq -r '.response.fileContent // empty' "${TMPFILE}")
  if [[ -z "${FILE_CONTENT}" ]]; then
    echo "[ERROR] Response body missing .response.fileContent"; exit 1
  fi
  echo "${FILE_CONTENT}" | base64 -d > "${OUTPUT_FILE}"
  unzip -t "${OUTPUT_FILE}" >/dev/null
  echo "[INFO] Package downloaded and verified: ${OUTPUT_FILE}"
else
  echo "[ERROR] HTTP ${HTTP_STATUS}"; exit 1
fi

API request payload reference

Field Required Description
packageType Yes The operating system of your runner. Accepted values: Linux or Windows.
userName No The AppViewX username to associate with the package. Defaults to SIGNPLUS_USERNAME if omitted.
authType Variant B only The authentication type configured in AppViewX. Accepted values: basicAuth or oAuth.
signingPolicy Variant B only A key-value pair that identifies the signing policy and certificate: { "<PolicyName>": "<username>=<certificateThumbprint>" }. Ask your AppViewX administrator for the certificate thumbprint.
connectionType No Set to Custom if signing requests must go through a load balancer or custom URL rather than the default AppViewX server address.
loadBalancerURL No The full URL of your load balancer, for example https://example.com:443. Include this when connectionType is Custom.

Step-by-step setup (Approach 2)

  1. Create scripts/download-signplus-api.sh using Variant A or Variant B.
  2. Create scripts/sign-with-signplus.sh using the content in The Signing Script.
  3. Create the workflow file and update SIGNPLUS_API_HOST, SIGNPLUS_API_PORT, and SIGNPLUS_POLICY_NAME.
  4. Add repository secrets in GitHub:
    • SIGNPLUS_USERNAME your AppViewX username.
    • SIGNPLUS_PASSWORD your AppViewX password.
    For Variant B, also add SIGNPLUS_POLICY_VALUE as a repository variable under the Variables tab.
  5. Push to trigger the workflow.

Approach 3: Use an OAuth service account token

Use this approach when your organization authenticates with AppViewX using OAuth service accounts. The workflow first retrieves a short-lived access token from AppViewX, then uses that token to download the SIGN+ package.

Step 1 Get a bearer token

A bearer token is a temporary access credential. Call the acctmgmt_get_service_token API to receive one:

TOKEN_RESPONSE=$(curl -k --location \
  "https://<your-appviewx-server>:<port>/avxapi/acctmgmt_get_service_token" \
  --header "Content-Type: application/json" \
  --data '{
    "payload": {
      "clientId": "<service-account-client-id>",
      "clientSecret": "<service-account-client-secret>"
    }
  }')

BEARER_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.response.token')
Important: Store the client ID and client secret as repository Secrets in GitHub at Settings > Security and quality > Secrets and variables > Actions. Never put them directly in workflow files or scripts.

Step 2 Download the SIGN+ package using the token

HTTP_STATUS=$(curl -k --location \
  "https://<your-appviewx-server>:<port>/avxapi/code-signing-download-csp-pkcs11-dll-sign?gwsource=external" \
  --header "Content-Type: application/json" \
  --header "Authorization: Bearer ${BEARER_TOKEN}" \
  --data '{"payload":{"packageType":"Linux"}}' \
  --output "${TMPFILE}" \
  -w '%{http_code}')

if [[ "${HTTP_STATUS}" -eq 200 ]]; then
  FILE_CONTENT=$(jq -r '.response.fileContent // empty' "${TMPFILE}")
  echo "${FILE_CONTENT}" | base64 -d > "${OUTPUT_FILE}"
  unzip -t "${OUTPUT_FILE}" >/dev/null
  echo "[INFO] Package downloaded and verified."
fi

After you download the package, run the signing script described in The Signing Script.

Note: Token lifetime depends on your AppViewX configuration. If tokens expire quickly, request a new token at the start of each workflow run.

Understanding the JSON configuration file

When SIGN+ installs, it generates a file named README_<PolicyName>.json in the installation directory. This file contains all the signing and verification commands for your policy pre-configured and ready to run.

Each command contains placeholder tokens that the signing script replaces at runtime:

Placeholder Replaced with
<input_file_path> Path to the unsigned artifact.
<output_file_path> Destination path for the signed artifact.
<signed_file_path> Path to the signed artifact (used in verification commands).

Supported signing tools

Tool JSON path Typical use
jarsigner .pkcs11.sign.jarsigner Java JAR files.
jsign .pkcs11.sign.jsign Windows PE, MSI, JAR, and other formats.
apksigner .pkcs11.sign.apksigner Android APK files.
xmlsectool .pkcs11.sign.xmlsectool XML documents.
OpenSSL (dgst) .pkcs11.sign.openssl_dgst Raw file signing.
OpenSSL (CMS) .pkcs11.sign.openssl_cms CMS / PKCS#7 detached signatures.
osslsigncode .pkcs11.sign.osslsigncode Windows Authenticode signing from a Linux runner.
cosign .pkcs11.sign.cosign OCI container images and blobs.
Maven .pkcs11.sign.maven_pom_snippet Maven build integration.
Gradle .pkcs11.sign.gradle_snippet Gradle build integration.
esptool .pkcs11.sign.esptool ESP32 / ESP8266 firmware signing.

Choose the right approach

Approach Best for Requires
1 Bundle in repository Most teams. Fastest, most reliable, no network dependency. Permission to commit a binary file to Git (or access to an artifact store).
2 Download via API Teams that cannot store binaries in Git. First download is cached. Network access to the AppViewX server from the runner.
3 OAuth service token Organizations that use OAuth service accounts for AppViewX authentication. Network access to the AppViewX server and an OAuth service account with download permissions.

Important notes and customization

The workflow files, scripts, and commands in this topic are reference implementations. Review and adapt them before using them in a production environment:

  • Policy name: Update SIGNPLUS_POLICY in the signing script to match the exact policy name configured in AppViewX for your environment.
  • Authentication type: Select oAuth or basicAuth based on how your AppViewX environment is configured.
  • Signing tool: The examples use jarsigner for Java JAR files. For other artifact types, read the corresponding key from the generated JSON file and use the same placeholder substitution pattern.
  • Platform: The scripts run on Linux using Bash. For Windows runners, use PowerShell with ConvertFrom-Json to read the JSON file.
  • TLS certificate verification: The download scripts use the -k flag in curl to skip TLS verification. For production environments, replace -k with --cacert /path/to/your/ca-bundle.crt to verify the AppViewX server certificate.
  • Secret management: Always store credentials as encrypted repository Secrets in GitHub so their values are masked in workflow logs. GitHub automatically redacts secret values from logs, but never print them intentionally.