Phase 3
Configure device certificate infrastructure
Create the division, device certificate profile, and certificate policy.
Phase details
- Workstream
- Device identity · Step 1 of 2
- Product
- Device Trust Manager
- 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- Prerequisite
- Prepare the private trust domain
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_idandservice_api_tokenfrom Establish the account foundation anddevice_ica_idfrom 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_2048for this server-side key-generation example.
Endpoints used
| Method | Path | Purpose |
|---|---|---|
| GET | /devicetrustmanager/certificate-configuration-service/api/v1/certificate-template | Select and validate the device template from Device Trust Manager. |
| GET | /devicetrustmanager/api/v4/rendezvous-zone | Select the division’s primary rendezvous zone. |
| POST | /devicetrustmanager/api/v4/division | Create the division. |
| GET | /devicetrustmanager/api/v4/division | Retrieve the created division ID. |
| POST | /devicetrustmanager/certificate-configuration-service/api/v1/certificate-profile | Create the device certificate profile. |
| POST | /devicetrustmanager/certificate-configuration-service/api/v2/certificate-policy | Create 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:
| Column | Required value |
|---|---|
name_matches | true. The name query parameter matches on substring, so a near miss returns the template but fails the filter. |
type | custom. A system template can only be cloned. |
format | x509 |
certificate_type | end_entity |
issue_types | Contains client_authentication |
key_types | Contains rsa_2048, or the key_gen object allows RSA with a size range that includes 2048 |
common_name_sources | Contains user_supplied |
required_ekus | Contains client_authentication |
available_to_account | true |
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.txtimport 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.jsonresponse = 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.jsonresponse = 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.jsonresponse = 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.idmatchesdevice_certificate_profile_id.ica.idmatches thedevice_ica_idselected when you prepared the private trust domain.division_idmatchesdivision_id.certificate_management_methodsincludesSINGLE.ca_connector_typeisdigicert_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 asDIGICERT_ONEon some resources anddigicert_oneon others. The only other value isdigicert_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_idwas 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_idand is available to the division. - The certificate policy binds the profile, division, and
device_ica_idand enablesSINGLEenrollment. - The policy supports the server-side key-generation request used to enroll and verify a device.
Next, enroll and verify a device.