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_aliaskeypair_idsigning_certificate_idsignature_idsigning-cert.pemtest-artifact.sig

Select a code-signing template in DigiCert® Software Trust Manager and create a private trust certificate profile. Then generate a test keypair, issue a private code-signing certificate, and sign a known SHA-256 hash. 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.
  • An active Software Trust Manager license for the target account that permits test-keypair generation and hash signing. Role permissions and template visibility do not prove that the account is licensed for these operations. Confirm the entitlement with your account administrator or DigiCert Support before creating resources.
  • A Software Trust Manager role that grants Manage certificate profiles (MANAGE_SM_CERTIFICATE_PROFILE), View certificate template (VIEW_SM_CERTIFICATE_TEMPLATE), View keypair (VIEW_SM_KEYPAIR), Generate keypair (GENERATE_SM_KEYPAIR), Generate certificate (GENERATE_SM_CERTIFICATE), and Sign (SIGN_SM_HASH). Those roles appear under the secure_software_manager key in the role list.
  • 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 license and template prerequisites before creating private code-signing resources. DigiCert controls certificate template creation and updates in hosted Software Trust Manager accounts. If Select the code-signing certificate template finds no eligible template, ask DigiCert Support to create one. Do not substitute a SYSTEM or PRODUCTION template 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.
GET/signingmanager/api/v1/keypairsAPI tokenRecover an uncertain create result by exact alias before retrying.
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 page. The name filter matches a contained string. The status, type, and template filters match the given value or category. The local filter checks every required value again before selecting a record.

CODE_SIGNING_TEMPLATE_NAME="Example private code signing template"

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

The loop writes the complete candidate set to signing-template-items.json. Apply the remaining eligibility and exact-match checks locally, and require one result:

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

Export the selected template ID and inspect the record before continuing:

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

Set the issuing CA in ca_certificate_profile_request.ca.id. The request does not accept an ica_id property at any level. An empty body array means that the profile adds no attribute overrides to the selected template. Generate a unique profile name so repeated validation runs do not collide.

SIGNING_PROFILE_NAME="private-code-signing-test-$(openssl rand -hex 6)"
printf '%s\n' "${SIGNING_PROFILE_NAME}" > signing-profile-name.txt
chmod 600 signing-profile-name.txt
export SIGNING_PROFILE_NAME

jq -n \
  --arg name "${SIGNING_PROFILE_NAME}" \
  --arg template_id "${CODE_SIGNING_CERTIFICATE_TEMPLATE_ID}" \
  --arg organization_id "${ORGANIZATION_ID}" \
  --arg ica_id "${SIGNING_ICA_ID}" '
  {
    name: $name,
    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

if ! jq -e \
  --arg name "${SIGNING_PROFILE_NAME}" \
  --arg template_id "${CODE_SIGNING_CERTIFICATE_TEMPLATE_ID}" \
  --arg organization_id "${ORGANIZATION_ID}" \
  --arg ica_id "${SIGNING_ICA_ID}" '
  .id and
  .name == $name and
  .status == "ACTIVE" and
  .type == "CA_PROFILE" and
  .ca_certificate_profile.profile == "TEST" and
  .ca_certificate_profile.certificate_template.id == $template_id and
  .ca_certificate_profile.organization.id == $organization_id and
  .ca_certificate_profile.ca.id == $ica_id' \
  signing-profile-response.json > /dev/null; then
  echo "Certificate profile response did not match the request." >&2
  exit 1
fi

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 secrets
from pathlib import Path

import requests

base_url = "https://demo.one.digicert.com"
headers = {"x-api-key": service_api_token}
signing_profile_name = f"private-code-signing-test-{secrets.token_hex(6)}"
Path("signing-profile-name.txt").write_text(
    signing_profile_name + "\n", encoding="utf-8"
)
Path("signing-profile-name.txt").chmod(0o600)

response = requests.post(
    f"{base_url}/signingmanager/api/v1/certificate-profiles",
    headers=headers,
    json={
        "name": signing_profile_name,
        "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()
ca_profile = profile.get("ca_certificate_profile", {})
if not all(
    (
        profile.get("id"),
        profile.get("name") == signing_profile_name,
        profile.get("status") == "ACTIVE",
        profile.get("type") == "CA_PROFILE",
        ca_profile.get("profile") == "TEST",
        ca_profile.get("certificate_template", {}).get("id")
        == code_signing_certificate_template_id,
        ca_profile.get("organization", {}).get("id") == organization_id,
        ca_profile.get("ca", {}).get("id") == signing_ica_id,
    )
):
    raise RuntimeError("Certificate profile response did not match the request")
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 example uses a software-backed RSA-3072 test key. Follow your organization’s signing policy for production key storage and access controls. Use hardware security module (HSM)-backed keys when required.

Generate and retain a unique alias before the create request. Use the alias to determine whether an uncertain request created the keypair without repeating the POST. Because expiry_type is optional, this TEST example omits it and does not request non-expiring behavior. Test keypairs have a maximum lifetime of 30 days.

KEYPAIR_ALIAS="private-trust-test-key-$(openssl rand -hex 6)"
printf '%s\n' "${KEYPAIR_ALIAS}" > keypair-alias.txt
chmod 600 keypair-alias.txt
export KEYPAIR_ALIAS

if jq -n --arg alias "${KEYPAIR_ALIAS}" '
  {
    alias: $alias,
    key_alg: "RSA",
    key_storage: "DISK",
    key_type: "TEST",
    key_modal_type: "STATIC",
    properties: {key_size: 3072},
    status: "ONLINE"
  }' |
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" \
  --data-binary @- \
  -o keypair-response.json; then
  if ! jq -e --arg alias "${KEYPAIR_ALIAS}" '
    .id and
    .alias == $alias and
    .key_alg == "RSA" and
    .key_size == 3072 and
    .key_type == "TEST" and
    .key_storage == "DISK" and
    .key_modal_type == "STATIC" and
    .key_status == "ONLINE"' keypair-response.json > /dev/null; then
    echo "Keypair response did not match the request." >&2
    exit 1
  fi
  KEYPAIR_ID="$(jq -er '.id' keypair-response.json)"
  export KEYPAIR_ID
  jq '{id, alias, key_alg, key_size, key_type, key_storage, key_modal_type,
       key_status, expiry_type, expire_on}' \
    keypair-response.json
else
  unset KEYPAIR_ID
  echo "Keypair creation was not confirmed. Do not retry; use the exact-alias recovery procedure below." >&2
  false
fi
import secrets
from pathlib import Path

client_certificate = ("client.crt", "client.key")
keypair_alias = f"private-trust-test-key-{secrets.token_hex(6)}"
Path("keypair-alias.txt").write_text(keypair_alias + "\n", encoding="utf-8")
Path("keypair-alias.txt").chmod(0o600)

response = requests.post(
    "https://clientauth.demo.one.digicert.com/signingmanager/api/v1/keypairs",
    headers=headers,
    cert=client_certificate,
    json={
        "alias": keypair_alias,
        "key_alg": "RSA",
        "key_storage": "DISK",
        "key_type": "TEST",
        "key_modal_type": "STATIC",
        "properties": {"key_size": 3072},
        "status": "ONLINE",
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(
        "Keypair creation was not confirmed. Do not retry; use the exact-alias recovery procedure."
    )
keypair = response.json()
if not all(
    (
        keypair.get("id"),
        keypair.get("alias") == keypair_alias,
        keypair.get("key_alg") == "RSA",
        keypair.get("key_size") == 3072,
        keypair.get("key_type") == "TEST",
        keypair.get("key_storage") == "DISK",
        keypair.get("key_modal_type") == "STATIC",
        keypair.get("key_status") == "ONLINE",
    )
):
    raise RuntimeError("Keypair response did not match the request")
keypair_id = keypair["id"]

Save the unique alias as keypair_alias and the response id as keypair_id. Inspect expiry_type and expire_on, and confirm in the response or Software Trust Manager that the keypair uses a finite test-expiry policy of no more than 30 days. Stop if it is non-expiring, exceeds that maximum, or cannot be confirmed.

Recover an uncertain keypair create result

Do not repeat the create request when it times out, loses its response, or returns a non-2xx status. A failed HTTP result does not prove that keypair creation rolled back. Search by the exact unique alias first.

curl --fail-with-body --silent --show-error --get \
  "https://demo.one.digicert.com/signingmanager/api/v1/keypairs" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  --data-urlencode "account_id=${ACCOUNT_ID}" \
  --data-urlencode "alias=${KEYPAIR_ALIAS}" \
  --data-urlencode "limit=100" \
  --data-urlencode "offset=0" \
  -o keypair-search.json

jq --arg alias "${KEYPAIR_ALIAS}" '
  [.items[] | select(.alias == $alias)] |
  if length == 1 then .[0]
  elif length == 0 then error("no matching keypair; investigate before retrying the create request")
  else error("multiple keypairs matched the supposedly unique alias")
  end' keypair-search.json > keypair-response.json

KEYPAIR_ID="$(jq -er '.id' keypair-response.json)"
export KEYPAIR_ID
if ! jq -e --arg alias "${KEYPAIR_ALIAS}" '
  .id and
  .alias == $alias and
  .key_alg == "RSA" and
  .key_size == 3072 and
  .key_type == "TEST" and
  .key_storage == "DISK" and
  .key_modal_type == "STATIC" and
  .key_status == "ONLINE"' keypair-response.json > /dev/null; then
  echo "Recovered keypair did not match the requested properties." >&2
  exit 1
fi
jq '{id, alias, key_alg, key_size, key_type, key_storage, key_modal_type,
     key_status, expiry_type, expire_on}' \
  keypair-response.json
lookup = requests.get(
    f"{base_url}/signingmanager/api/v1/keypairs",
    headers=headers,
    params={
        "account_id": account_id,
        "alias": keypair_alias,
        "limit": 100,
        "offset": 0,
    },
    timeout=30,
)
lookup.raise_for_status()
matches = [item for item in lookup.json()["items"] if item["alias"] == keypair_alias]
if len(matches) != 1:
    raise RuntimeError(
        f"expected one exact keypair match before retry; observed {len(matches)}"
    )
keypair = matches[0]
if not all(
    (
        keypair.get("id"),
        keypair.get("key_alg") == "RSA",
        keypair.get("key_size") == 3072,
        keypair.get("key_type") == "TEST",
        keypair.get("key_storage") == "DISK",
        keypair.get("key_modal_type") == "STATIC",
        keypair.get("key_status") == "ONLINE",
    )
):
    raise RuntimeError("Recovered keypair did not match the requested properties")
keypair_id = keypair["id"]

Use this recovery path only to resolve an uncertain result, not as an automatic retry loop. If the search returns no match, preserve the create response, request timestamp, and correlation information before deciding whether a new create request is safe. If the response reports a license error, stop and resolve the target account’s Software Trust Manager entitlement before continuing.

Step 5.4: Issue the private code-signing certificate

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

CERTIFICATE_ALIAS="${KEYPAIR_ALIAS}-certificate"
export CERTIFICATE_ALIAS

jq -n \
  --arg profile_id "${SIGNING_PROFILE_ID}" \
  --arg alias "${CERTIFICATE_ALIAS}" '
  {
    certificate_profile: {id: $profile_id},
    alias: $alias,
    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

Protect the response, retain the certificate ID, and inspect its non-secret metadata:

chmod 600 signing-certificate-response.json
if ! jq -e \
  --arg alias "${CERTIFICATE_ALIAS}" \
  --arg profile_id "${SIGNING_PROFILE_ID}" '
  .id and
  .alias == $alias and
  .certificate_status == "ACTIVE" and
  .default_cert == true and
  .enrollment_method == "CM_GENERATE" and
  .hierarchy == "PRIVATE" and
  .certificate_profile.id == $profile_id and
  (.cert | type == "string" and length > 0)' \
  signing-certificate-response.json > /dev/null; then
  echo "Signing certificate response did not match the request." >&2
  exit 1
fi
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, cert: (.cert | .[0:24] + "...")}' \
  signing-certificate-response.json

The response also contains cert, the issued certificate as a base64-encoded DER string. The projection above truncates it so the summary stays readable. Save the response id as signing_certificate_id, then decode cert and validate the certificate’s path and purpose:

jq -er '.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

if ! jq -e '
  (.id | type == "string" and length > 0) and
  (.signature | type == "string" and length > 0)' \
  signing-response.json > /dev/null; then
  echo "Signing response did not contain a signature ID and value." >&2
  exit 1
fi
jq '{id, signature: (.signature | .[0:24] + "...")}' signing-response.json
SIGNATURE_ID="$(jq -er '.id' signing-response.json)"
export SIGNATURE_ID

The response contains id and signature. Decode the signature string from the documented default Base64 representation for Software Trust signing tools. Then verify the RSA PKCS#1 v1.5 signature over the SHA-256 digest:

jq -er '.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 private key corresponding to signing-cert.pem produced the returned signature. 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. You can address the same keypair from the Signing Manager Controller (smctl) command line. The smctl sign sign-hash command accepts either a Base64 digest through --hash or a file through --file. Signature output is Base64 by default unless you request --binary. Sign binaries using a keypair alias and configuration file addresses the keypair by the unique alias you saved as KEYPAIR_ALIAS.

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. Do not treat this step as a template for a build-pipeline signing stage. Production signing lists what a release pipeline must define in addition to key access and signature generation.

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 target account has an active Software Trust Manager license that permits test-keypair generation and hash signing.
  • The CA_PROFILE response identifies the intended issuing CA for code signing, organization, and Software Trust Manager template.
  • Keypair creation used a unique retained alias plus the API token and matching client certificate over the clientauth. host; any uncertain result was resolved by exact-alias lookup before retrying.
  • 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.

You have validated the private signing and signature path.