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

In this phase, you select a certificate template from DigiCert® Device Trust Manager and create the resources that define device-certificate issuance. The certificate policy binds that 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.
  • A Device Trust Manager role that contains Solution administrator.
  • 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.
  • 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. Your tenant’s API uses a record offset and accepts at most 100 records per page for this endpoint.

The eligibility check supports both template formats found in the current API reference and API responses: the key_types list and the key_gen object with allowed key types and an RSA size range.

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

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

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.

Step 3.2: Create and retrieve the division

Each division requires a primary rendezvous zone. Find an enabled zone that is assigned to the account for primary usage before you create the division. The create response contains status information but not the new division ID. Create the division, then search by account and exact name and read the ID from the matching item in records.

DIVISION_NAME="Private trust devices"
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

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}\"
  }"

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

base_url = "https://demo.one.digicert.com"
headers = {"x-api-key": service_api_token}
division_name = "Private trust devices"
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"]

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

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

The body property is an array of profile-attribute objects, not an array of strings. The API requires explicit key-type, validity, and renewal settings in addition to the user-supplied common name. This example limits the profile to RSA-2048 and uses a one-month validity period. It also enables the documented renewal settings and limits the profile to the division created when you create and retrieve the division. Adjust the validity and renewal values to match your certificate policy.

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. The API requires single_cert_request_parameters for this method. This guide selects server-side RSA-2048 key generation for the demo request used to request the device certificate with the passcode. For production devices with secure key stores, prefer device-generated keys and certificate signing requests (CSRs).

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

Validate the response 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 identifies the DigiCert ONE CA connection.

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.