Phase 2
Prepare the private trust domain
Assign the issuing CAs and download the CA certificates used by both workloads.
Phase details
- Workstream
- Shared foundation · Step 2 of 2
- Product
- DigiCert Private CA
- Time
- 25 minutes
- Setup
- One-time setup
- Goal
- Identify the root and workload-specific issuing CAs, assign each issuer to the account, and download the CA certificates.
- Inputs
account_idservice_api_tokenroot_ca_namedevice_ica_namesigning_ica_name- Expected outputs
root_ca_iddevice_ica_idsigning_ica_idroot-ca.pemdevice-issuing-ca.pemsigning-issuing-ca.pem- Prerequisite
- Establish the account foundation
Use DigiCert® Private CA to identify the private root CA and the issuing CA for each workload. Assign the issuers to the target account, then download the CA certificates used for path validation. You can select the same issuing CA for both workloads when your public key infrastructure (PKI) policy permits it.
The root CA certificate is the trust anchor. The issuing CAs sign end-entity certificates. Certificate-template selection happens in the consuming product because DigiCert® Device Trust Manager and DigiCert® Software Trust Manager expose separate template resources and identifiers.
Phase prerequisites
Make sure you have:
- The
account_idandservice_api_tokenfrom Establish the account foundation. - An active private root CA and an active issuing CA for each workload beneath that root.
- The exact approved names for the root CA, device issuing CA, and signing issuing CA.
- A service-user role that grants the DigiCert Private CA View CA (
VIEW_CM_CA) and Manage CA accounts (MANAGE_CM_CA_ACCOUNTS) permissions. Those roles appear under theca_managerkey in the role list. - The approved SHA-256 fingerprint for the root CA certificate.
Endpoints used
| Method | Path | Purpose |
|---|---|---|
| GET | /certificate-authority/api/v1/ca | List accessible CAs and inspect account assignments. |
| GET | /certificate-authority/api/v1/ca/{id} | Retrieve an approved CA directly and confirm a new account assignment. |
| POST | /certificate-authority/api/v1/ca/{id}/accounts | Assign an issuing CA to the account. |
| GET | /certificate-authority/api/v1/ca/{id}/download?format=pem | Download a CA certificate in PEM format. |
Step 2.1: Select the root and issuing CAs
GET /certificate-authority/api/v1/ca returns items, limit, offset, and total. The offset value is the zero-based index of the first returned item. The total value is the number of CAs that the request can access.
The API reference defines no query parameters for this list operation. Before selecting a CA, the following guard confirms that total equals the number of returned items. If the values differ, the list is truncated. Do not select a CA from it. Use one of these recovery options:
- If your PKI owner provides the approved CA IDs through a trusted channel, retrieve each record with
GET /certificate-authority/api/v1/ca/{id}. This is the preferred fallback because that operation is documented and avoids the list. - If your tenant supports
limitandoffseton this operation, page the list only after confirming from the returnedlimitandoffsetvalues that the service honored them. The API reference does not document those request parameters for this operation. - If you have neither a complete list nor approved IDs, stop and ask your PKI owner for the IDs.
Set the exact approved names and retrieve the inventory. The completeness guard stops before selecting a CA from a truncated response:
ROOT_CA_NAME="Example private root"
DEVICE_ICA_NAME="Example device issuing CA"
SIGNING_ICA_NAME="Example signing issuing CA"
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-o ca-inventory.json
jq -e '
if .total == (.items | length) then true
else error("CA list is incomplete; confirm deployed paging parameters")
end' ca-inventory.json > /dev/nullimport requests
base_url = "https://demo.one.digicert.com"
headers = {"x-api-key": service_api_token}
root_ca_name = "Example private root"
device_ica_name = "Example device issuing CA"
signing_ica_name = "Example signing issuing CA"
response = requests.get(
f"{base_url}/certificate-authority/api/v1/ca",
headers=headers,
timeout=30,
)
response.raise_for_status()
page = response.json()
ca_records = page["items"]
if page["total"] != len(ca_records):
raise RuntimeError("CA list is incomplete. Confirm deployed paging parameters")Select one active record for each exact name and retain the IDs:
select_active_ca_id() {
jq -er --arg name "$1" '
[.items[] | select(.name == $name and .status == "active")] |
if length == 1 then .[0].id
else error("expected one active CA named " + $name)
end' ca-inventory.json
}
ROOT_CA_ID="$(select_active_ca_id "${ROOT_CA_NAME}")"
DEVICE_ICA_ID="$(select_active_ca_id "${DEVICE_ICA_NAME}")"
SIGNING_ICA_ID="$(select_active_ca_id "${SIGNING_ICA_NAME}")"
export ROOT_CA_ID DEVICE_ICA_ID SIGNING_ICA_ID
printf '%s\n' "${ROOT_CA_ID}" > root-ca-id.txt
printf '%s\n' "${DEVICE_ICA_ID}" > device-ica-id.txt
printf '%s\n' "${SIGNING_ICA_ID}" > signing-ica-id.txtactive_cas = [item for item in ca_records if item.get("status") == "active"]
def select_one_ca(name):
matches = [item for item in active_cas if item.get("name") == name]
if len(matches) != 1:
raise RuntimeError(f"Expected one active CA named {name}. Found {len(matches)}")
return matches[0]
root = select_one_ca(root_ca_name)
device_issuer = select_one_ca(device_ica_name)
signing_issuer = select_one_ca(signing_ica_name)
root_ca_id = root["id"]
device_ica_id = device_issuer["id"]
signing_ica_id = signing_issuer["id"]Validate that the records form the expected root-and-issuer hierarchy. Stop if this check fails:
jq -e --arg root_id "${ROOT_CA_ID}" \
--arg device_id "${DEVICE_ICA_ID}" \
--arg signing_id "${SIGNING_ICA_ID}" '
def selected($id): first(.items[] | select(.id == $id));
selected($root_id) as $root |
selected($device_id) as $device |
selected($signing_id) as $signing |
if
($root.status == "active") and
($device.status == "active") and
($signing.status == "active") and
((($root.cert_type // $root.ca_type // "") | ascii_downcase) == "root") and
((($device.cert_type // $device.ca_type // "") | ascii_downcase) == "intermediate") and
($device.configuration.issue_end_entities == true) and
($device.issuer.id == $root.id) and
((($signing.cert_type // $signing.ca_type // "") | ascii_downcase) == "intermediate") and
($signing.configuration.issue_end_entities == true) and
($signing.issuer.id == $root.id)
then true
else error("selected CAs do not form an end-entity issuing hierarchy")
end
' ca-inventory.json > /dev/nulldef ca_kind(ca):
return (ca.get("cert_type") or ca.get("ca_type") or "").lower()
if ca_kind(root) != "root":
raise RuntimeError("The selected trust anchor is not a root CA")
for label, issuer in (("device", device_issuer), ("signing", signing_issuer)):
if ca_kind(issuer) != "intermediate":
raise RuntimeError(f"The selected {label} issuer is not an intermediate CA")
if (issuer.get("configuration") or {}).get("issue_end_entities") is not True:
raise RuntimeError(f"The selected {label} issuer cannot issue end entities")
if (issuer.get("issuer") or {}).get("id") != root_ca_id:
raise RuntimeError(f"The selected {label} issuer is not beneath the selected root")Inspect the selected records before you assign either issuer to the account:
jq --arg root_id "${ROOT_CA_ID}" \
--arg device_id "${DEVICE_ICA_ID}" \
--arg signing_id "${SIGNING_ICA_ID}" '
[.items[] |
select(.id == $root_id or .id == $device_id or .id == $signing_id) |
{
id,
name,
status,
ca_type,
cert_type,
issue_end_entities: .configuration.issue_end_entities,
issuer,
account_assignments,
extensions
}]
' ca-inventory.json
If the list guard fails and you have approved IDs, replace the incomplete inventory with the three directly retrieved records, then run the same hierarchy validation shown above before continuing:
# Set these only from the IDs approved by your PKI owner.
ROOT_CA_ID="<approved-root-ca-id>"
DEVICE_ICA_ID="<approved-device-issuer-id>"
SIGNING_ICA_ID="<approved-signing-issuer-id>"
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${ROOT_CA_ID}" \
-H "x-api-key: ${SERVICE_API_TOKEN}" -o root-ca-record.json
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${DEVICE_ICA_ID}" \
-H "x-api-key: ${SERVICE_API_TOKEN}" -o device-ica-record.json
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${SIGNING_ICA_ID}" \
-H "x-api-key: ${SERVICE_API_TOKEN}" -o signing-ica-record.json
Combine the directly retrieved records into the same inventory shape used by the normal path, then retain the IDs:
jq -s 'unique_by(.id) | {items: ., limit: length, offset: 0, total: length}' \
root-ca-record.json device-ica-record.json signing-ica-record.json \
> ca-inventory.json
export ROOT_CA_ID DEVICE_ICA_ID SIGNING_ICA_ID
printf '%s\n' "${ROOT_CA_ID}" > root-ca-id.txt
printf '%s\n' "${DEVICE_ICA_ID}" > device-ica-id.txt
printf '%s\n' "${SIGNING_ICA_ID}" > signing-ica-id.txt
Before continuing, verify the selected records:
- The root has a root
cert_typeorca_type. - Each issuer has an intermediate
cert_typeorca_type, and itsconfiguration.issue_end_entitiesvalue istrue. - Each issuer’s
issuer.idequals the selected root CA ID. - The CA status is
active. - Inspect each issuer’s
extensionswhen the response includes them. Confirm the constraints against the downloaded CA certificate, and document a compensating control when the issuer is not constrained to one workload.
Do not infer code-signing eligibility from a top-level issue_types value. The end-entity template’s extended key usage controls that purpose, and you validate it when you select the code-signing certificate template.
Step 2.2: Assign each issuer to the account
Inspect each issuer’s account_assignments before adding the account. If an assignment is already present, do not repeat the request that adds it.
assign_ica_if_needed() {
local ica_id="$1"
if jq -e --arg ica_id "${ica_id}" --arg account_id "${ACCOUNT_ID}" '
any(.items[];
.id == $ica_id and
any(.account_assignments[]?;
(if type == "object" then .id else . end) == $account_id))
' ca-inventory.json > /dev/null; then
printf 'ICA %s is already assigned to account %s.\n' "${ica_id}" "${ACCOUNT_ID}"
return
fi
curl --fail-with-body --silent --show-error \
-X POST \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${ica_id}/accounts" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"accounts\": [\"${ACCOUNT_ID}\"]}"
}
assign_ica_if_needed "${DEVICE_ICA_ID}"
if [[ "${SIGNING_ICA_ID}" != "${DEVICE_ICA_ID}" ]]; then
assign_ica_if_needed "${SIGNING_ICA_ID}"
fi
For a new assignment, a successful response returns 204 No Content. Include each distinct, unassigned issuing CA ID only once.
The assignment does not appear in the copy of the inventory you already downloaded, so retrieve each issuer again and confirm that its account_assignments contains account_id:
verify_ica_assignment() {
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/$1" \
-H "x-api-key: ${SERVICE_API_TOKEN}" |
jq -er --arg account_id "${ACCOUNT_ID}" '
if any(.account_assignments[]?;
(if type == "object" then .id else . end) == $account_id)
then {id, name, account_assignments}
else error("ICA " + .id + " is not assigned to the target account")
end'
}
verify_ica_assignment "${DEVICE_ICA_ID}"
if [[ "${SIGNING_ICA_ID}" != "${DEVICE_ICA_ID}" ]]; then
verify_ica_assignment "${SIGNING_ICA_ID}"
fi
Step 2.3: Download and validate the CA certificates
Download the root and workload-specific issuing CA certificates. Install the root certificate as the trust anchor in relying-party trust stores. Provide the appropriate issuing CA as an intermediate when a verifier needs it to construct the path.
Use format=pem to save each response as a PEM certificate. Stop if OpenSSL cannot parse a downloaded file. Never automate trust-store distribution without validating the certificate and its approved fingerprint.
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${ROOT_CA_ID}/download?format=pem" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--output root-ca.pem
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${DEVICE_ICA_ID}/download?format=pem" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--output device-issuing-ca.pem
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/certificate-authority/api/v1/ca/${SIGNING_ICA_ID}/download?format=pem" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--output signing-issuing-ca.pem
Inspect and validate the certificates before distribution:
openssl x509 -in root-ca.pem -noout -subject -issuer -serial -fingerprint -sha256 \
-ext basicConstraints,keyUsage,extendedKeyUsage
openssl x509 -in device-issuing-ca.pem -noout -subject -issuer -serial -fingerprint -sha256 \
-ext basicConstraints,keyUsage,extendedKeyUsage
openssl x509 -in signing-issuing-ca.pem -noout -subject -issuer -serial -fingerprint -sha256 \
-ext basicConstraints,keyUsage,extendedKeyUsage
openssl verify -CAfile root-ca.pem device-issuing-ca.pem
openssl verify -CAfile root-ca.pem signing-issuing-ca.pem
Compare each issuing CA’s extensions with the purpose it serves. Many private issuing CAs carry no extended key usage (EKU) extension. When the extension is absent, OpenSSL prints no line for it. A missing X509v3 Extended Key Usage block is therefore a result, not a failed command. Confirm whether each issuer is constrained, and record the result either way.
An issuing CA that carries an EKU extension constrains the EKUs available to the certificates beneath it, which makes workload separation a property of the hierarchy rather than a configuration convention. A device issuing CA constrained to client authentication cannot issue a code-signing certificate that verifiers accept for that purpose.
EKU constraint on a CA certificate is a deployed convention rather than a requirement of RFC 5280, so enforcement depends on the verifier. Confirm that your relying parties enforce it before you treat it as the only control. The basicConstraints path length limits how many intermediates can follow the CA in a path.
root-ca.pem through your approved configuration-management process and verify its SHA-256 fingerprint through a separate trusted channel. Do not trust a downloaded certificate until you validate its identity.Phase 2 checkpoint
-
root_ca_ididentifies the expected active root CA. -
device_ica_idandsigning_ica_ididentify active intermediates beneath that root. - Each distinct issuing CA is assigned to
account_id, confirmed by retrieving the CA again after the assignment. -
device-issuing-ca.pemandsigning-issuing-ca.pemvalidate toroot-ca.pem. - Each issuing CA’s extensions were inspected, and each issuer is constrained to the workload it serves or a compensating control is recorded.
- The root fingerprint matches the value approved for distribution.
The shared foundation is complete. Next, begin the device identity workstream, begin the code-signing workstream, or work on both in parallel. Both workstreams are required.