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
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

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_id and service_api_token from 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 the ca_manager key in the role list.
  • The approved SHA-256 fingerprint for the root CA certificate.

Endpoints used

MethodPathPurpose
GET/certificate-authority/api/v1/caList 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}/accountsAssign an issuing CA to the account.
GET/certificate-authority/api/v1/ca/{id}/download?format=pemDownload 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:

  1. 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.
  2. If your tenant supports limit and offset on this operation, page the list only after confirming from the returned limit and offset values that the service honored them. The API reference does not document those request parameters for this operation.
  3. 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/null
import 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.txt
active_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/null
def 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_type or ca_type.
  • Each issuer has an intermediate cert_type or ca_type, and its configuration.issue_end_entities value is true.
  • Each issuer’s issuer.id equals the selected root CA ID.
  • The CA status is active.
  • Inspect each issuer’s extensions when 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.

Distribute 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_id identifies the expected active root CA.
  • device_ica_id and signing_ica_id identify 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.pem and signing-issuing-ca.pem validate to root-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.