Phase 5

Configure and test private code signing

Issue a private code-signing certificate, sign a known hash, and verify the signature and certificate path.

Phase details

Workstream
Code signing · Step 1 of 1
Time
30 minutes
Setup
Mixed setup and verification
Goal
Select a code-signing template, bind the signing issuer to a profile, and verify a signature.
Inputs
account_idorganization_idservice_api_tokensigning_ica_idcode_signing_template_nameclient.crtclient.keyroot-ca.pemsigning-issuing-ca.pem
Expected outputs
code_signing_certificate_template_idsigning_profile_idkeypair_idsigning_certificate_idsignature_id

In this phase, you select a code-signing template in DigiCert® Software Trust Manager and create a private trust certificate profile. You then generate a test keypair, issue a private code-signing certificate, and sign a known SHA-256 hash. After signing, you verify both the signature and the certificate path.

Keypair creation and signing require multi-factor authentication. For those operations, send both factors:

  • The service user’s API token in x-api-key.
  • The same service user’s client certificate and private key over mutual TLS (mTLS).

Use the clientauth. host for those protected operations. A client certificate without the API token is not sufficient for this Software Trust Manager workflow.

Phase prerequisites

Make sure you have:

  • account_id, organization_id, and service_api_token from Establish the account foundation and signing_ica_id from Prepare the private trust domain.
  • client.crt and its matching client.key for the service user.
  • root-ca.pem and signing-issuing-ca.pem from Prepare the private trust domain.
  • A Software Trust Manager role with Manage certificate profiles, View certificate template, View keypair, Generate keypair, Generate certificate, and Sign permissions.
  • An active certificate template with type: CUSTOM and template: TEST available to the target account. The template must require the code-signing extended key usage (EKU).
Verify the template prerequisite before you create any private code-signing resources. In a hosted Software Trust Manager account, DigiCert controls certificate template creation and updates. If Select the code-signing certificate template finds no eligible template, ask DigiCert Support to create one. Do not use a SYSTEM or PRODUCTION template as a substitute for this test workflow.

Endpoints used

MethodPathAuthenticationPurpose
GET/signingmanager/api/v1/certificate-templatesAPI tokenSelect and validate the signing template from Software Trust Manager.
POST/signingmanager/api/v1/certificate-profilesAPI tokenCreate the private trust certificate profile.
POST/signingmanager/api/v1/keypairsAPI token and mTLSCreate the test signing keypair.
POST/signingmanager/api/v1/keypairs/{keypair_id}/certificatesAPI tokenIssue a certificate onto the keypair.
POST/signingmanager/api/v1/keypairs/{keypair_id}/signAPI token and mTLSSign a base64-encoded hash.

Step 5.1: Select the code-signing certificate template

List the templates in the target account. The response stores records in items. This endpoint uses a page index for offset, so the loop increments the value by one for each additional page.

printf '[]\n' > signing-template-items.json
page_index=0

while :; do
  curl --fail-with-body --silent --show-error --get \
    "https://demo.one.digicert.com/signingmanager/api/v1/certificate-templates" \
    -H "x-api-key: ${SERVICE_API_TOKEN}" \
    --data-urlencode "account_id=${ACCOUNT_ID}" \
    --data-urlencode "name=${CODE_SIGNING_TEMPLATE_NAME}" \
    --data-urlencode "status=ACTIVE" \
    --data-urlencode "type=CUSTOM" \
    --data-urlencode "template=TEST" \
    --data-urlencode "limit=100" \
    --data-urlencode "offset=${page_index}" \
    -o signing-template-page.json

  jq -s '.[0] + .[1].items' \
    signing-template-items.json signing-template-page.json \
    > signing-template-items.next.json
  mv signing-template-items.next.json signing-template-items.json

  total="$(jq -er '.total' signing-template-page.json)"
  page_count="$(jq -er '.items | length' signing-template-page.json)"
  collected="$(jq -er 'length' signing-template-items.json)"
  if (( collected >= total )); then
    break
  fi
  if (( page_count == 0 )); then
    echo "Software Trust pagination ended before total records were collected." >&2
    exit 1
  fi
  page_index=$((page_index + 1))
done

jq --arg name "${CODE_SIGNING_TEMPLATE_NAME}" --arg account_id "${ACCOUNT_ID}" '
    [.[] |
      select(
        .name == $name and
        .status == "ACTIVE" and
        .type == "CUSTOM" and
        .template == "TEST" and
        (.body.issue_types | index("code_signing")) and
        (.body.extensions.extended_key_usage.required_usages |
          index("code_signing")) and
        ((.accounts | length) == 0 or
          any(.accounts[]; .id == $account_id))
      )] |
    if length == 1 then .[0]
    else error("expected one eligible code-signing certificate template")
    end |
    {id, name, status, type, template, accounts, body}' \
  signing-template-items.json > signing-template.json

CODE_SIGNING_CERTIFICATE_TEMPLATE_ID="$(jq -er '.id' signing-template.json)"
export CODE_SIGNING_CERTIFICATE_TEMPLATE_ID
jq '{id, name, status, type, template, accounts, body}' signing-template.json

The example exports the selected id as CODE_SIGNING_CERTIFICATE_TEMPLATE_ID.

Do not use a template ID from DigiCert® Private CA or DigiCert® Device Trust Manager. Each product uses separate template resources, so the IDs are not interchangeable.

Step 5.2: Create the private trust certificate profile

The current Software Trust Manager API request format represents the issuing CA as ca_certificate_profile_request.ca.id. Use that nested object rather than a top-level ica_id property.

jq -n \
  --arg template_id "${CODE_SIGNING_CERTIFICATE_TEMPLATE_ID}" \
  --arg organization_id "${ORGANIZATION_ID}" \
  --arg ica_id "${SIGNING_ICA_ID}" '
  {
    name: "Private code signing test",
    profile_type: "CA_PROFILE",
    auto_renewal: "DISABLED",
    rekey: "DISABLED",
    ca_certificate_profile_request: {
      certificate_template_id: $template_id,
      profile: "TEST",
      body: [],
      organization: {id: $organization_id},
      ca: {id: $ica_id}
    }
  }' |
curl --fail-with-body --silent --show-error \
  -X POST "https://demo.one.digicert.com/signingmanager/api/v1/certificate-profiles" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  -o signing-profile-response.json

SIGNING_PROFILE_ID="$(jq -er '.id' signing-profile-response.json)"
export SIGNING_PROFILE_ID
jq '{id, name, status, type, ca_certificate_profile}' signing-profile-response.json
import requests

base_url = "https://demo.one.digicert.com"
headers = {"x-api-key": service_api_token}

response = requests.post(
    f"{base_url}/signingmanager/api/v1/certificate-profiles",
    headers=headers,
    json={
        "name": "Private code signing test",
        "profile_type": "CA_PROFILE",
        "auto_renewal": "DISABLED",
        "rekey": "DISABLED",
        "ca_certificate_profile_request": {
            "certificate_template_id": code_signing_certificate_template_id,
            "profile": "TEST",
            "body": [],
            "organization": {"id": organization_id},
            "ca": {"id": signing_ica_id},
        },
    },
    timeout=30,
)
response.raise_for_status()
profile = response.json()
signing_profile_id = profile["id"]

Confirm that the response is active, has type CA_PROFILE, and identifies the intended template, organization, and issuing CA for code signing. Save the response id as signing_profile_id.

Step 5.3: Create a test keypair with both authentication factors

This validation example uses a software-backed RSA-3072 test key. Follow your organization’s signing policy for production signing-key storage and access controls. Use hardware security module (HSM)-backed keys when required.

curl --fail-with-body --silent --show-error \
  -X POST "https://clientauth.demo.one.digicert.com/signingmanager/api/v1/keypairs" \
  --cert client.crt \
  --key client.key \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "private-trust-test-key",
    "key_alg": "RSA",
    "key_storage": "DISK",
    "key_type": "TEST",
    "properties": {
      "key_size": 3072
    },
    "status": "ONLINE",
    "expiry_type": "NO_EXPIRY"
  }' \
  -o keypair-response.json

KEYPAIR_ID="$(jq -er '.id' keypair-response.json)"
export KEYPAIR_ID
jq '{id, alias, key_alg, key_size, key_type, key_storage, key_status}' keypair-response.json
client_certificate = ("client.crt", "client.key")

response = requests.post(
    "https://clientauth.demo.one.digicert.com/signingmanager/api/v1/keypairs",
    headers=headers,
    cert=client_certificate,
    json={
        "alias": "private-trust-test-key",
        "key_alg": "RSA",
        "key_storage": "DISK",
        "key_type": "TEST",
        "properties": {"key_size": 3072},
        "status": "ONLINE",
        "expiry_type": "NO_EXPIRY",
    },
    timeout=30,
)
response.raise_for_status()
keypair = response.json()
keypair_id = keypair["id"]

Save the response id as keypair_id.

Step 5.4: Issue the private code-signing certificate

Reference the profile by ID and make the certificate the keypair’s default certificate.

umask 077

jq -n \
  --arg profile_id "${SIGNING_PROFILE_ID}" '
  {
    certificate_profile: {id: $profile_id},
    alias: "private-trust-test-certificate",
    default_cert: true
  }' |
curl --fail-with-body --silent --show-error \
  -X POST \
  "https://demo.one.digicert.com/signingmanager/api/v1/keypairs/${KEYPAIR_ID}/certificates" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  -o signing-certificate-response.json

chmod 600 signing-certificate-response.json
SIGNING_CERTIFICATE_ID="$(jq -er '.id' signing-certificate-response.json)"
export SIGNING_CERTIFICATE_ID
jq '{id, alias, certificate_status, default_cert, enrollment_method, hierarchy, certificate_profile, valid_from, valid_to}' \
  signing-certificate-response.json

Save the response id as signing_certificate_id. Extract the base64-encoded DER certificate and validate its path and purpose:

jq -r '.cert' signing-certificate-response.json |
  openssl base64 -d -A -out signing-cert.der

openssl x509 -inform DER -in signing-cert.der -out signing-cert.pem
openssl verify -CAfile root-ca.pem -untrusted signing-issuing-ca.pem signing-cert.pem
openssl x509 -in signing-cert.pem -noout -subject -issuer -serial -dates -purpose

Confirm that the certificate contains the code-signing EKU and chains through the expected issuing CA for code signing to root-ca.pem.

Step 5.5: Sign and verify a known hash

Create a test artifact and its binary SHA-256 digest. Base64-encode the digest for the API request.

printf '%s' 'private-trust signature verification' > test-artifact.txt
openssl dgst -sha256 -binary test-artifact.txt > test-artifact.sha256
HASH_BASE64="$(openssl base64 -A -in test-artifact.sha256)"

Sign the digest with both authentication factors:

jq -n \
  --arg hash "${HASH_BASE64}" \
  '{hash: $hash, sig_alg: "SHA256WithRSA"}' |
curl --fail-with-body --silent --show-error \
  -X POST \
  "https://clientauth.demo.one.digicert.com/signingmanager/api/v1/keypairs/${KEYPAIR_ID}/sign" \
  --cert client.crt \
  --key client.key \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  -o signing-response.json

jq '{id}' signing-response.json
SIGNATURE_ID="$(jq -er '.id' signing-response.json)"
export SIGNATURE_ID

Extract and verify the RSA PKCS#1 v1.5 signature over the SHA-256 digest:

jq -r '.signature' signing-response.json |
  openssl base64 -d -A -out test-artifact.sig

openssl x509 -in signing-cert.pem -pubkey -noout > signing-public-key.pem

openssl pkeyutl -verify \
  -pubin \
  -inkey signing-public-key.pem \
  -in test-artifact.sha256 \
  -sigfile test-artifact.sig \
  -pkeyopt digest:sha256

A successful verification proves that the returned signature was produced by the private key corresponding to signing-cert.pem. The earlier openssl verify command independently proves that the signing certificate chains to the approved private root.

This is a low-level API verification, not a platform-specific signed artifact. Use smctl, KSP, PKCS#11, or the applicable signing integration to create and verify Authenticode, JAR, container, or other release artifacts. Timestamping and release-policy enforcement are separate production requirements.

Phase 5 checkpoint

  • The selected Software Trust Manager template is active, custom, configured for testing, available to the account, and requires the code-signing EKU.
  • The CA_PROFILE response identifies the intended issuing CA for code signing, organization, and Software Trust Manager template.
  • Keypair creation used the API token and matching client certificate over the clientauth. host.
  • The signing certificate has the code-signing EKU and validates to root-ca.pem through signing-issuing-ca.pem.
  • Signing used both authentication factors.
  • OpenSSL verified the returned signature with the signing certificate’s public key.

The private signing and signature-validation path is validated.