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
- Product
- Software Trust Manager
- 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- Prerequisite
- Prepare the private trust domain
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, andservice_api_tokenfrom Establish the account foundation andsigning_ica_idfrom Prepare the private trust domain.client.crtand its matchingclient.keyfor the service user.root-ca.pemandsigning-issuing-ca.pemfrom 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 thesecure_software_managerkey in the role list. - An active certificate template with
type: CUSTOMandtemplate: TESTavailable to the target account. The template must require the code-signing extended key usage (EKU).
SYSTEM or PRODUCTION template for this test workflow.Endpoints used
| Method | Path | Authentication | Purpose |
|---|---|---|---|
| GET | /signingmanager/api/v1/certificate-templates | API token | Select and validate the signing template from Software Trust Manager. |
| POST | /signingmanager/api/v1/certificate-profiles | API token | Create the private trust certificate profile. |
| POST | /signingmanager/api/v1/keypairs | API token and mTLS | Create the test signing keypair. |
| GET | /signingmanager/api/v1/keypairs | API token | Recover an uncertain create result by exact alias before retrying. |
| POST | /signingmanager/api/v1/keypairs/{keypair_id}/certificates | API token | Issue a certificate onto the keypair. |
| POST | /signingmanager/api/v1/keypairs/{keypair_id}/sign | API token and mTLS | Sign 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.jsonimport 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
fiimport 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.jsonlookup = 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_PROFILEresponse 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.pemthroughsigning-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.
- If device identity is incomplete, configure the device certificate infrastructure.
- If both workstreams are validated, prepare the solution for production operations.