Phase 3

Configure device certificate infrastructure

Create the division, device certificate profile, and certificate policy.

Phase details

Workstream
Device identity · Step 1 of 2
Time
20 minutes
Setup
One-time setup
Goal
Select a device template, create a profile limited to one division, and bind the device issuing CA in a REST certificate policy.
Inputs
account_idservice_api_tokendevice_ica_iddevice_template_nameprimary_rendezvous_zone_name
Expected outputs
device_certificate_template_idprimary_rendezvous_zone_iddivision_iddevice_certificate_profile_idcertificate_policy_id

Select a certificate template from DigiCert® Device Trust Manager, then create the resources that define device-certificate issuance. The certificate policy binds the profile to the device issuing CA and enables the SINGLE REST enrollment method.

When you enroll and verify a device, you add passcode authentication through the device group’s policy assignment.

Phase prerequisites

Make sure you have:

  • account_id and service_api_token from Establish the account foundation and device_ica_id from Prepare the private trust domain.
  • The active Device Trust Manager Solution Administrator role returned for your tenant. This is a role assignment, not an effective permission code.
  • An active custom X.509 end-entity template available to the target account.
  • The exact name of the approved device certificate template.
  • The exact name of an enabled rendezvous zone assigned to the account for primary usage. DigiCert operates and assigns the zones, so you select one that already exists.
  • A template that requires the client authentication extended key usage (EKU), permits a user-supplied common name, and permits rsa_2048 for this server-side key-generation example.

Endpoints used

MethodPathPurpose
GET/devicetrustmanager/certificate-configuration-service/api/v1/certificate-templateSelect and validate the device template from Device Trust Manager.
GET/devicetrustmanager/api/v4/rendezvous-zoneSelect the division’s primary rendezvous zone.
POST/devicetrustmanager/api/v4/divisionCreate the division.
GET/devicetrustmanager/api/v4/divisionRetrieve the created division ID.
POST/devicetrustmanager/certificate-configuration-service/api/v1/certificate-profileCreate the device certificate profile.
POST/devicetrustmanager/certificate-configuration-service/api/v2/certificate-policyCreate the REST certificate policy and bind the issuing CA.

Step 3.1: Select the device certificate template

List the templates available to the account. The response stores template records in records, not items. The offset value is the index of the first record, not a page index. The endpoint defaults to 20 records and accepts a limit of up to 1000, so the following loop sets limit explicitly.

The name parameter matches templates whose name contains the value, so the filter re-checks the name for an exact match. There is no server-side filter for certificate_type, the EKU, the key type, or the common-name source, so the filter checks those in jq.

The key-type condition accepts both supported record shapes. The Device Trust Manager API reference defines body.key_types as a flat list. Product documentation also shows templates whose body contains a key_gen object with allowed key types and an RSA size range. The check accepts either shape.

DEVICE_TEMPLATE_NAME="Example device authentication template"

printf '[]\n' > device-template-records.json
record_offset=0

while :; do
  curl --fail-with-body --silent --show-error --get \
    "https://demo.one.digicert.com/devicetrustmanager/certificate-configuration-service/api/v1/certificate-template" \
    -H "x-api-key: ${SERVICE_API_TOKEN}" \
    --data-urlencode "account_id=${ACCOUNT_ID}" \
    --data-urlencode "name=${DEVICE_TEMPLATE_NAME}" \
    --data-urlencode "status=ACTIVE" \
    --data-urlencode "type=custom" \
    --data-urlencode "format=x509" \
    --data-urlencode "limit=100" \
    --data-urlencode "offset=${record_offset}" \
    -o device-template-page.json

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

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

The loop writes every candidate returned for the server-side filters to device-template-records.json. Apply the remaining eligibility checks locally and require exactly one match:

jq --arg name "${DEVICE_TEMPLATE_NAME}" --arg account_id "${ACCOUNT_ID}" '
    [.[] |
      select(
        .name == $name and
        .status == "ACTIVE" and
        .type == "custom" and
        .format == "x509" and
        .certificate_type == "end_entity" and
        (.body.issue_types | index("client_authentication")) and
        (
          (((.body.key_types // []) | index("rsa_2048")) != null) or
          (
            .body.key_gen.enabled == true and
            (((.body.key_gen.key_type.allowed_types // []) |
              map(ascii_downcase) | index("rsa")) != null) and
            ((.body.key_gen.rsa_key_size.min_bits // 2147483647) <= 2048) and
            ((.body.key_gen.rsa_key_size.max_bits // 0) >= 2048)
          )
        ) and
        any(.body.subject.attributes[];
          .type == "common_name" and
          (.allowed_source | index("user_supplied"))) and
        any(.body.extensions.extended_key_usage.required_usages[];
          .oid == "client_authentication") and
        ((.limit_by_accounts == false) or
          any(.accounts[]; .id == $account_id))
      )] |
    if length == 1 then .[0]
    else error("expected one eligible device certificate template")
    end |
    {id, name, status, type, format, certificate_type, accounts, body}' \
  device-template-records.json > device-template.json

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

DEVICE_CERTIFICATE_TEMPLATE_ID="$(jq -er '.id' device-template.json)"
export DEVICE_CERTIFICATE_TEMPLATE_ID
jq '{id, name, status, type, format, certificate_type, accounts, body}' device-template.json

Save the selected id as device_certificate_template_id. Do not use a template ID returned by DigiCert® Private CA or another product. Each product uses separate template resources, so the IDs are not interchangeable.

If no template is eligible

The filter reports expected one eligible device certificate template whether the name matched nothing or a matching template failed one of the other conditions. List the account’s templates without the name filter and evaluate each condition separately to find out which one failed:

curl --fail-with-body --silent --show-error --get \
  "https://demo.one.digicert.com/devicetrustmanager/certificate-configuration-service/api/v1/certificate-template" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  --data-urlencode "account_id=${ACCOUNT_ID}" \
  --data-urlencode "status=ACTIVE" \
  --data-urlencode "limit=1000" |
jq --arg name "${DEVICE_TEMPLATE_NAME}" --arg account_id "${ACCOUNT_ID}" '
  .records |
  map({
    id,
    name,
    name_matches: (.name == $name),
    type,
    format,
    certificate_type,
    issue_types: .body.issue_types,
    key_types: (.body.key_types // .body.key_gen),
    common_name_sources: [
      .body.subject.attributes[]? |
      select(.type == "common_name") | .allowed_source[]?
    ],
    required_ekus: [
      .body.extensions.extended_key_usage.required_usages[]? |
      if type == "object" then .oid else . end
    ],
    available_to_account: ((.limit_by_accounts == false) or
      any(.accounts[]?; .id == $account_id))
  })'

Read the output against the conditions the filter requires:

ColumnRequired value
name_matchestrue. The name query parameter matches on substring, so a near miss returns the template but fails the filter.
typecustom. A system template can only be cloned.
formatx509
certificate_typeend_entity
issue_typesContains client_authentication
key_typesContains rsa_2048, or the key_gen object allows RSA with a size range that includes 2048
common_name_sourcesContains user_supplied
required_ekusContains client_authentication
available_to_accounttrue
This workflow does not change certificate templates. If no template satisfies every condition, do not relax a condition to make an ineligible template pass. Ask an authorized template administrator whether they can clone or edit a template in your deployment. Enabling, disabling, deleting, and restoring templates can require system-scoped access. If no customer administrator has the required access, ask DigiCert Support or your account representative. The profile and policy inherit the template’s constraints, so an unsuitable template produces device certificates that relying parties reject.

Step 3.2: Create and retrieve the division

A division is an isolated environment inside the account that scopes device groups, certificate profiles, certificate policies, and software updates. Create one to keep this workflow’s profile, policy, and group from affecting other work in a shared demo tenant.

Each division requires a primary rendezvous zone and can name a second zone as a backup. DigiCert operates the rendezvous zones and assigns them to your account, so select an existing zone. Accounts and divisions share a common set of zones. Every account has a default division that already names a primary and secondary zone. If the following list returns no enabled zone for the account, contact DigiCert. Do not attempt to create a zone.

The create response contains status information but not the new division ID. Select the rendezvous zone, create the division, then retrieve the new record by account and exact name.

Select the primary rendezvous zone

Select one enabled zone that is available for primary usage in the account:

PRIMARY_RENDEZVOUS_ZONE_NAME="Example primary rendezvous zone"

PRIMARY_RENDEZVOUS_ZONE_ID="$(
  curl --fail-with-body --silent --show-error --get \
    "https://demo.one.digicert.com/devicetrustmanager/api/v4/rendezvous-zone" \
    -H "x-api-key: ${SERVICE_API_TOKEN}" \
    --data-urlencode "account_id=${ACCOUNT_ID}" \
    --data-urlencode "name=${PRIMARY_RENDEZVOUS_ZONE_NAME}" \
    --data-urlencode "status=ENABLED" \
    --data-urlencode "is_primary_usage=true" \
    --data-urlencode "limit=100" |
  jq -er --arg name "${PRIMARY_RENDEZVOUS_ZONE_NAME}" '
    [.records[] |
      select(.name == $name and .status == "ENABLED" and .is_primary_usage == true)] |
    if length == 1 then .[0].id
    else error("expected one enabled primary rendezvous zone")
    end'
)"
export PRIMARY_RENDEZVOUS_ZONE_ID
printf '%s\n' "${PRIMARY_RENDEZVOUS_ZONE_ID}" > primary-rendezvous-zone-id.txt
import requests

base_url = "https://demo.one.digicert.com"
headers = {"x-api-key": service_api_token}
primary_rendezvous_zone_name = "Example primary rendezvous zone"

response = requests.get(
    f"{base_url}/devicetrustmanager/api/v4/rendezvous-zone",
    headers=headers,
    params={
        "account_id": account_id,
        "name": primary_rendezvous_zone_name,
        "status": "ENABLED",
        "is_primary_usage": True,
        "limit": 100,
    },
    timeout=30,
)
response.raise_for_status()
matches = [
    item for item in response.json()["records"]
    if item["name"] == primary_rendezvous_zone_name
    and item["status"] == "ENABLED"
    and item["is_primary_usage"] is True
]
if len(matches) != 1:
    raise RuntimeError(f"Expected one enabled primary rendezvous zone. Found {len(matches)}")
primary_rendezvous_zone_id = matches[0]["id"]

Create the division

Create the isolated division with the selected zone. This request changes tenant state:

DIVISION_NAME="Private trust devices"

curl --fail-with-body --silent --show-error \
  -X POST "https://demo.one.digicert.com/devicetrustmanager/api/v4/division" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"${DIVISION_NAME}\",
    \"description\": \"Devices enrolled from the private trust stack\",
    \"account_id\": \"${ACCOUNT_ID}\",
    \"primary_rzone_id\": \"${PRIMARY_RENDEZVOUS_ZONE_ID}\"
  }"
division_name = "Private trust devices"

response = requests.post(
    f"{base_url}/devicetrustmanager/api/v4/division",
    headers=headers,
    json={
        "name": division_name,
        "description": "Devices enrolled from the private trust stack",
        "account_id": account_id,
        "primary_rzone_id": primary_rendezvous_zone_id,
    },
    timeout=30,
)
response.raise_for_status()

If the request result is uncertain, search by the exact division name before you repeat the POST.

Retrieve and verify the division

Retrieve the active division by exact name, then save its ID:

curl --fail-with-body --silent --show-error --get \
  "https://demo.one.digicert.com/devicetrustmanager/api/v4/division" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  --data-urlencode "account_id=${ACCOUNT_ID}" \
  --data-urlencode "name=${DIVISION_NAME}" |
  jq --arg name "${DIVISION_NAME}" \
    '[.records[] | select(.name == $name and .status == "ACTIVE")] |
     if length == 1 then .[0] else error("expected one active division") end' \
  > division.json

DIVISION_ID="$(jq -er '.id' division.json)"
export DIVISION_ID
jq '{id, name, status, account}' division.json
response = requests.get(
    f"{base_url}/devicetrustmanager/api/v4/division",
    headers=headers,
    params={"account_id": account_id, "name": division_name},
    timeout=30,
)
response.raise_for_status()
matches = [
    item for item in response.json()["records"]
    if item["name"] == division_name and item["status"] == "ACTIVE"
]
if len(matches) != 1:
    raise RuntimeError(f"Expected one active division. Found {len(matches)}")
division_id = matches[0]["id"]

Save the selected rendezvous-zone id as primary_rendezvous_zone_id and the retrieved division id as division_id.

Step 3.3: Create the device certificate profile

Send the body property as an array of profile-attribute objects, not strings. In addition to the user-supplied common name, the API requires explicit key-type, validity, and renewal settings.

This example limits the profile to RSA-2048, enables the documented renewal settings, and limits the profile to the division created in Create and retrieve the division. It uses a one-month validity period so test certificates expire on their own. Adjust the validity and renewal values to match your certificate policy. Production lifetimes depend on the certificate’s role. A bootstrap certificate is provisioned once and is typically long-lived. An operational certificate is renewed routinely and is typically short-lived, which limits exposure without depending on the device to check a revocation list.

curl --fail-with-body --silent --show-error \
  -X POST \
  "https://demo.one.digicert.com/devicetrustmanager/certificate-configuration-service/api/v1/certificate-profile" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary "$(
    jq -n \
      --arg account_id "${ACCOUNT_ID}" \
      --arg template_id "${DEVICE_CERTIFICATE_TEMPLATE_ID}" \
      --arg division_id "${DIVISION_ID}" '
      {
        name: "Private device identity",
        account_id: $account_id,
        certificate_template_id: $template_id,
        body: [
          {key: "allow_any_key_type", optional: false, enabled: true,
            sources: ["fixed_value"], value: "no"},
          {key: "allowed_key_types", optional: false, enabled: true,
            sources: ["fixed_value"], value: ["rsa_2048"]},
          {key: "subject.common_name", optional: false, enabled: true,
            sources: ["user_supplied"], value: ""},
          {key: "validity.duration_unit", optional: false, enabled: true,
            sources: ["fixed_value"], value: "months"},
          {key: "validity.duration_value", optional: false, enabled: true,
            sources: ["fixed_value"], value: 1},
          {key: "renewal_settings.renew_valid_cert", optional: false, enabled: true,
            sources: ["fixed_value"], value: "anytime"},
          {key: "renewal_settings.renew_expired_cert", optional: false, enabled: true,
            sources: ["fixed_value"], value: "anytime"},
          {key: "renewal_settings.renew_revoked_cert", optional: false, enabled: true,
            sources: ["fixed_value"], value: true},
          {key: "renewal_settings.renewal_key_pair", optional: false, enabled: true,
            sources: ["fixed_value"], value: "optional"}
        ],
        divisions: [$division_id]
      }'
  )" \
  -o device-profile-response.json

DEVICE_CERTIFICATE_PROFILE_ID="$(jq -er '.id' device-profile-response.json)"
export DEVICE_CERTIFICATE_PROFILE_ID
jq '{id, name, status, certificate_template, divisions}' device-profile-response.json
response = requests.post(
    f"{base_url}/devicetrustmanager/certificate-configuration-service/api/v1/certificate-profile",
    headers=headers,
    json={
        "name": "Private device identity",
        "account_id": account_id,
        "certificate_template_id": device_certificate_template_id,
        "body": [
            {"key": "allow_any_key_type", "optional": False, "enabled": True,
             "sources": ["fixed_value"], "value": "no"},
            {"key": "allowed_key_types", "optional": False, "enabled": True,
             "sources": ["fixed_value"], "value": ["rsa_2048"]},
            {"key": "subject.common_name", "optional": False, "enabled": True,
             "sources": ["user_supplied"], "value": ""},
            {"key": "validity.duration_unit", "optional": False, "enabled": True,
             "sources": ["fixed_value"], "value": "months"},
            {"key": "validity.duration_value", "optional": False, "enabled": True,
             "sources": ["fixed_value"], "value": 1},
            {"key": "renewal_settings.renew_valid_cert", "optional": False,
             "enabled": True, "sources": ["fixed_value"], "value": "anytime"},
            {"key": "renewal_settings.renew_expired_cert", "optional": False,
             "enabled": True, "sources": ["fixed_value"], "value": "anytime"},
            {"key": "renewal_settings.renew_revoked_cert", "optional": False,
             "enabled": True, "sources": ["fixed_value"], "value": True},
            {"key": "renewal_settings.renewal_key_pair", "optional": False,
             "enabled": True, "sources": ["fixed_value"], "value": "optional"},
        ],
        "divisions": [division_id],
    },
    timeout=30,
)
response.raise_for_status()
profile = response.json()
device_certificate_profile_id = profile["id"]

Confirm that the response references the intended template and division. Save the response id as device_certificate_profile_id.

Step 3.4: Create the REST certificate policy

Create a policy that uses the profile, division, issuing CA, and SINGLE enrollment method. Include single_cert_request_parameters, which the API requires for this method. This guide selects server-side RSA-2048 key generation for the demo request used to register the device with the passcode. For production devices with secure key stores, prefer device-generated keys and certificate signing requests (CSRs). For policies that issue against device-generated keys, key_generation_option also accepts client_side and client_or_server_side.

curl --fail-with-body --silent --show-error \
  -X POST \
  "https://demo.one.digicert.com/devicetrustmanager/certificate-configuration-service/api/v2/certificate-policy" \
  -H "x-api-key: ${SERVICE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary "$(
    jq -n \
      --arg division_id "${DIVISION_ID}" \
      --arg profile_id "${DEVICE_CERTIFICATE_PROFILE_ID}" \
      --arg ica_id "${DEVICE_ICA_ID}" '
      {
        certificate_policy: {
          name: "Private device REST enrollment",
          division_id: $division_id,
          certificate_profile_id: $profile_id,
          ica_id: $ica_id,
          certificate_management_methods: ["SINGLE"],
          key_generation_option: "server_side",
          key_generation_type: "RSA_2048",
          key_generation_allow_to_change: false,
          require_approval_for_enroll: false,
          require_approval_for_renew: false,
          single_cert_request_parameters: {
            key_generation_option: "server_side",
            key_type: "rsa_2048",
            key_generation_allowed_to_change: false,
            allow_to_use_pregenerated_keys: false,
            private_key_format: "pem",
            rsa_private_key_syntax: "pkcs8",
            allow_key_cache: false,
            response_with_certificate_only: false,
            split_certificate_response: true,
            include_chain_option: "include_ica_and_root"
          }
        }
      }'
  )" \
  -o certificate-policy-response.json

CERTIFICATE_POLICY_ID="$(jq -er '.certificate_policy.id' certificate-policy-response.json)"
export CERTIFICATE_POLICY_ID
jq '.certificate_policy |
    {id, name, status, division_id, certificate_profile, ica,
     ca_connector_type, certificate_management_methods, single_cert_request_parameters}' \
  certificate-policy-response.json
response = requests.post(
    f"{base_url}/devicetrustmanager/certificate-configuration-service/api/v2/certificate-policy",
    headers=headers,
    json={
        "certificate_policy": {
            "name": "Private device REST enrollment",
            "division_id": division_id,
            "certificate_profile_id": device_certificate_profile_id,
            "ica_id": device_ica_id,
            "certificate_management_methods": ["SINGLE"],
            "key_generation_option": "server_side",
            "key_generation_type": "RSA_2048",
            "key_generation_allow_to_change": False,
            "require_approval_for_enroll": False,
            "require_approval_for_renew": False,
            "single_cert_request_parameters": {
                "key_generation_option": "server_side",
                "key_type": "rsa_2048",
                "key_generation_allowed_to_change": False,
                "allow_to_use_pregenerated_keys": False,
                "private_key_format": "pem",
                "rsa_private_key_syntax": "pkcs8",
                "allow_key_cache": False,
                "response_with_certificate_only": False,
                "split_certificate_response": True,
                "include_chain_option": "include_ica_and_root",
            },
        }
    },
    timeout=30,
)
response.raise_for_status()
policy = response.json()["certificate_policy"]
certificate_policy_id = policy["id"]

Confirm these response values before continuing:

  • certificate_profile.id matches device_certificate_profile_id.
  • ica.id matches the device_ica_id selected when you prepared the private trust domain.
  • division_id matches division_id.
  • certificate_management_methods includes SINGLE.
  • ca_connector_type is digicert_one, which is the connector for a CA in DigiCert® Private CA. Compare the value without regard to case, because Device Trust Manager returns it as DIGICERT_ONE on some resources and digicert_one on others. The only other value is digicert_cis, the CertCentral CIS connector, which means the policy is bound to a public CertCentral CA rather than your private hierarchy.

Phase 3 checkpoint

  • An active division exists and division_id was retrieved from the list response.
  • The division was created with the approved enabled primary rendezvous zone.
  • The selected Device Trust Manager template is active, custom, X.509, and eligible for this client authentication request.
  • The certificate profile uses device_certificate_template_id and is available to the division.
  • The certificate policy binds the profile, division, and device_ica_id and enables SINGLE enrollment.
  • The policy supports the server-side key-generation request used to enroll and verify a device.

Next, enroll and verify a device.