Phase 4
Enroll and verify a device
Create passcode enrollment configuration, register a managed device, and validate its bootstrap certificate.
Phase details
- Workstream
- Device identity · Step 2 of 2
- Product
- Device Trust Manager
- Time
- 25 minutes
- Setup
- Mixed setup and verification
- Goal
- Attach a passcode authentication policy to a device group, register a managed device, and validate its bootstrap key and certificate path.
- Inputs
account_idservice_api_tokendivision_idcertificate_policy_idenrollment_passcoderoot-ca.pemdevice-issuing-ca.pem- Expected outputs
authentication_policy_idpasscode_iddevice_group_iddevice_iddevice_certificate_idcertificate_request_iddevice.pemdevice.key- Prerequisite
- Configure device certificate infrastructure
Create an authentication policy in DigiCert® Device Trust Manager and add a passcode to it. Assign the authentication and certificate policies to a device group, then register a managed device with the passcode. Verify that the returned private key matches the bootstrap certificate and that the certificate path terminates at the root trust anchor prepared for the private trust domain.
Phase prerequisites
Make sure you have:
account_idandservice_api_tokenfrom Establish the account foundation anddivision_idandcertificate_policy_idfrom Configure the device certificate infrastructure.root-ca.pemanddevice-issuing-ca.pemfrom 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.
- A passcode that contains six to 64 characters and is generated and stored through an approved secret-management process.
- A secure destination for the server-generated device private key.
Endpoints used
| Method | Path | Purpose |
|---|---|---|
| POST | /devicetrustmanager/authentication-service/api/v1/authentication-policy | Create the authentication policy. |
| POST | /devicetrustmanager/authentication-service/api/v1/passcode | Create the enrollment passcode. |
| POST | /devicetrustmanager/api/v4/device-group | Create the group and policy assignment. |
| GET | /devicetrustmanager/api/v4/device-group | Retrieve the group ID and inspect the assignment. |
| POST | /devicetrustmanager/api/v4/device/registration | Register the managed device and issue its bootstrap certificate. |
| GET | /devicetrustmanager/api/v4/device | Verify the registered device in the intended group. |
| GET | /devicetrustmanager/certificate-issuance-service/api/v2/certificate | Retrieve the issued certificate and request IDs. |
Step 4.1: Create an authentication policy
jq -n \
--arg account_id "${ACCOUNT_ID}" '
{
name: "Private device enrollment",
account_id: $account_id,
description: "Passcode authentication for the private device REST policy"
}' |
curl --fail-with-body --silent --show-error \
-X POST \
"https://demo.one.digicert.com/devicetrustmanager/authentication-service/api/v1/authentication-policy" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @- \
-o authentication-policy-response.json
AUTHENTICATION_POLICY_ID="$(jq -er '.id' authentication-policy-response.json)"
export AUTHENTICATION_POLICY_ID
jq '{id, name, account_id, status}' authentication-policy-response.json
Save the response id as authentication_policy_id.
Step 4.2: Create a passcode credential
Set enable_username_for_passcode to false because this workflow does not use a username. The API defaults this property to true and requires a username when it is enabled. Supply a passcode with six to 64 characters through a protected prompt. Do not put it in shell history or logs.
If this phase is running in a new shell, restrict access to every file created from this point forward:
umask 077
The setting applies to newly created files in the current shell. Prompt for the passcode without displaying it, then validate its length:
read -rsp "Enrollment passcode: " ENROLLMENT_PASSCODE
echo
if (( ${#ENROLLMENT_PASSCODE} < 6 || ${#ENROLLMENT_PASSCODE} > 64 )); then
echo "The enrollment passcode must contain 6-64 characters." >&2
return 1 2>/dev/null || exit 1
fi
Create the passcode credential. The request sends the passcode directly from the shell variable and does not write it to a request file:
jq -n \
--arg name "Private device enrollment passcode" \
--arg account_id "${ACCOUNT_ID}" \
--arg authentication_policy_id "${AUTHENTICATION_POLICY_ID}" \
--arg passcode "${ENROLLMENT_PASSCODE}" \
'{
name: $name,
account_id: $account_id,
authentication_policy_id: $authentication_policy_id,
enable_username_for_passcode: false,
passcode: $passcode,
usage_limit: 10
}' |
curl --fail-with-body --silent --show-error \
-X POST \
"https://demo.one.digicert.com/devicetrustmanager/authentication-service/api/v1/passcode" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @- \
-o passcode-response.json
Protect and inspect the response without printing a returned passcode value:
chmod 600 passcode-response.json
PASSCODE_ID="$(jq -er '.id' passcode-response.json)"
export PASSCODE_ID
jq '{id, name, authentication_policy_id, enable_username_for_passcode, passcode_hint}' \
passcode-response.json
Your tenant’s API might return the passcode in the response. Do not depend on that behavior. Treat the entire response as a secret, do not print the passcode property, and continue using the original ENROLLMENT_PASSCODE value from your secret store. Save the response id as passcode_id.
Step 4.3: Create and retrieve the device group
A device must belong to a device group before Device Trust Manager can manage it. The device inherits the group’s policy assignments as soon as it joins and cannot exist outside a group.
Use the required is_static property to select the membership model. A static group has explicit membership. Devices are added individually during enrollment or by an administrative action, and each device belongs to exactly one static group at a time. This workflow requires a static group because enrollment places the device in the group named in the request. The disruptive policy types used for firmware and configuration updates can also apply only to static groups.
The policies array contains policy-assignment objects, not policy-ID strings. The assignment below marks the certificate policy as the bootstrap certificate policy and applies the authentication-policy override documented for device groups.
The bootstrapCertificate assignment defines how the device’s initial certificate is issued and managed. A complete device lifecycle adds an operationalCertificate assignment to the same group. Operational certificates are short-lived, can be revoked, and use the bootstrap credential for enrollment. A device group can hold both assignments, but this workflow validates bootstrap issuance only. Register a single device demonstrates a comparable bootstrap registration flow and identifies operational certificate issuance as a subsequent step. It does not create the operational policy for you.
DEVICE_GROUP_NAME="Private trust test devices"
curl --fail-with-body --silent --show-error \
-X POST "https://demo.one.digicert.com/devicetrustmanager/api/v4/device-group" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${DEVICE_GROUP_NAME}\",
\"description\": \"Validation group for private device certificates\",
\"division_id\": \"${DIVISION_ID}\",
\"account_id\": \"${ACCOUNT_ID}\",
\"is_static\": true,
\"policies\": [
{
\"policy_id\": \"${CERTIFICATE_POLICY_ID}\",
\"assignment_name\": \"Private device bootstrap certificate\",
\"type\": \"bootstrapCertificate\",
\"auth_policy_id\": \"${AUTHENTICATION_POLICY_ID}\",
\"status\": \"ACTIVE\"
}
]
}"
The create response does not provide the group ID. If the request result is uncertain, perform this lookup before repeating the POST. Retrieve the active group by exact name, verify that there is one match, and retain its ID:
curl --fail-with-body --silent --show-error --get \
"https://demo.one.digicert.com/devicetrustmanager/api/v4/device-group" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--data-urlencode "account_id=${ACCOUNT_ID}" \
--data-urlencode "division_id=${DIVISION_ID}" \
--data-urlencode "name=${DEVICE_GROUP_NAME}" \
--data-urlencode "include_assignment=true" |
jq --arg name "${DEVICE_GROUP_NAME}" \
'[.records[] | select(.name == $name and .status == "ACTIVE")] |
if length == 1 then .[0] else error("expected one active device group") end' \
> device-group.json
DEVICE_GROUP_ID="$(jq -er '.id' device-group.json)"
export DEVICE_GROUP_ID
jq '{id, name, status, policies}' device-group.json
Save the exact matching record’s id as device_group_id. Confirm its policy assignment contains the intended certificate_policy_id and authentication_policy_id and reports status: ACTIVE.
Step 4.4: Register the device with the passcode
Passcode authentication is an alternative to authentication with an API token for the device-registration endpoint. Send x-passcode, and omit x-api-key. The request creates the device in device_group_id and issues the bootstrap certificate defined by the group’s policy assignment.
This demo uses the server-side key-generation policy created when you configure the device certificate infrastructure. The registration contract uses uppercase key_type, key_format, and key_syntax enum values. For production devices with protected key storage, configure client-side generation and submit a certificate signing request (CSR) instead.
Use a unique device name so you can resolve an uncertain registration result without repeating the POST:
DEVICE_NAME="device-$(date -u +%Y%m%d-%H%M%S)-$$.example.internal"
printf '%s\n' "${DEVICE_NAME}" > device-name.txt
export DEVICE_NAME
jq -n \
--arg name "${DEVICE_NAME}" \
--arg account_id "${ACCOUNT_ID}" \
--arg device_group_id "${DEVICE_GROUP_ID}" \
--arg certificate_policy_id "${CERTIFICATE_POLICY_ID}" \
'{
name: $name,
description: "Private trust validation device",
account_id: $account_id,
device_group_id: $device_group_id,
certificate_policies: {
bootstrap: [
{
server_side_key_gen: true,
certificate_policy_id: $certificate_policy_id,
key_type: "RSA_2048",
key_format: "PEM",
key_syntax: "PKCS8",
attributes: [
{name: "subject.common_name", value: $name}
]
}
]
}
}' |
curl --fail-with-body --silent --show-error \
-X POST \
"https://demo.one.digicert.com/devicetrustmanager/api/v4/device/registration" \
-H "x-passcode: ${ENROLLMENT_PASSCODE}" \
-H "Content-Type: application/json" \
--data-binary @- \
-o device-registration-response.json
The response contains the server-generated private key, so keep the complete response protected. Require the registered device state and exactly one bootstrap result for the requested policy before extracting any secret fields:
chmod 600 device-registration-response.json
jq -e \
--arg name "${DEVICE_NAME}" \
--arg certificate_policy_id "${CERTIFICATE_POLICY_ID}" '
[.private_keys[]? |
select(.policy_id == $certificate_policy_id and .policy_type == "BOOTSTRAP")] as $bootstrap |
if (.device_id | type) == "string" and
.device_name == $name and
.device_status == "REGISTERED" and
.operational_status == "ENABLED" and
($bootstrap | length) == 1 and
($bootstrap[0].certificate | type) == "string" and
($bootstrap[0].private_key | type) == "string"
then {
device_id,
device_name,
device_status,
operational_status,
bootstrap_results: ($bootstrap | length)
}
else error("device registration response failed validation")
end' device-registration-response.json
DEVICE_ID="$(jq -er '.device_id' device-registration-response.json)"
printf '%s\n' "${DEVICE_ID}" > device-id.txt
export DEVICE_ID
A successful response has this structure. It does not contain a certificate ID, certificate-request ID, or certificate chain.
{
"device_id": "<device-uuid>",
"device_name": "device-<unique-run-label>.example.internal",
"device_status": "REGISTERED",
"operational_status": "ENABLED",
"private_keys": [
{
"policy_id": "IOT_<uuid>",
"policy_type": "BOOTSTRAP",
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
}
]
}
Verify that the device appears in the intended group:
curl --fail-with-body --silent --show-error --get \
"https://demo.one.digicert.com/devicetrustmanager/api/v4/device" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--data-urlencode "account_id=${ACCOUNT_ID}" \
--data-urlencode "device_group_id=${DEVICE_GROUP_ID}" \
--data-urlencode "name=${DEVICE_NAME}" \
--data-urlencode "limit=100" \
--data-urlencode "offset=0" |
jq --arg id "${DEVICE_ID}" --arg name "${DEVICE_NAME}" '
[.records[]? | select(.id == $id and .name == $name)] |
if length == 1 then .[0]
else error("expected one registered device in the intended group")
end' > device-record.json
The registration response does not return the certificate and request IDs retained by this manual. Retrieve them with the service API token. Omit device_group_id from this certificate query because that filter can exclude the registered certificate.
curl --fail-with-body --silent --show-error --get \
"https://demo.one.digicert.com/devicetrustmanager/certificate-issuance-service/api/v2/certificate" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
--data-urlencode "account_id=${ACCOUNT_ID}" \
--data-urlencode "division_id=${DIVISION_ID}" \
--data-urlencode "certificate_policy_id=${CERTIFICATE_POLICY_ID}" \
--data-urlencode "common_name=${DEVICE_NAME}" \
--data-urlencode "limit=100" \
--data-urlencode "offset=0" |
jq --arg id "${DEVICE_ID}" --arg name "${DEVICE_NAME}" '
[.records[]? |
select(.common_name == $name and .device.id == $id)] |
if length == 1 then .[0]
else error("expected one certificate for the registered device")
end' > device-certificate.json
DEVICE_CERTIFICATE_ID="$(jq -er '.id' device-certificate.json)"
CERTIFICATE_REQUEST_ID="$(jq -er '.certificate_request_id' device-certificate.json)"
export DEVICE_CERTIFICATE_ID CERTIFICATE_REQUEST_ID
POST if its result is uncertain. Search the device inventory by the exact unique name retained in device-name.txt first. If the device exists, registration succeeded; do not create a duplicate. If the response containing the server-generated private key was lost, stop the full key-validation path and follow your approved recovery or cleanup process. See Device registration fails or has an uncertain result.Step 4.5: Validate the key and certificate path
Extract the sensitive response fields without printing them:
jq -er --arg certificate_policy_id "${CERTIFICATE_POLICY_ID}" '
[.private_keys[]? |
select(.policy_id == $certificate_policy_id and .policy_type == "BOOTSTRAP")] |
if length == 1 then .[0].certificate
else error("expected one bootstrap certificate")
end' device-registration-response.json > device.pem
jq -er --arg certificate_policy_id "${CERTIFICATE_POLICY_ID}" '
[.private_keys[]? |
select(.policy_id == $certificate_policy_id and .policy_type == "BOOTSTRAP")] |
if length == 1 then .[0].private_key
else error("expected one bootstrap private key")
end' device-registration-response.json > device.key
chmod 600 device.pem device.key
Confirm the private key matches the end-entity certificate and validate its path to the approved root.
The registration response does not return the certificate chain. Validate against root-ca.pem and device-issuing-ca.pem from Prepare the private trust domain. The separately downloaded, fingerprint-verified root proves that the device certificate chains to the trust anchor you approved for distribution.
test "$(openssl x509 -in device.pem -pubkey -noout | openssl sha256)" = \
"$(openssl pkey -in device.key -pubout | openssl sha256)"
openssl verify \
-CAfile root-ca.pem \
-untrusted device-issuing-ca.pem \
device.pem
openssl x509 -in device.pem -noout -subject -issuer -serial -dates -purpose
Confirm that the subject matches the requested device identity and that the certificate purpose and extensions match the approved device template. Protect the demo private key while you retain validation evidence, then securely remove it according to your secret-handling policy. Do not deploy this server-generated test key as a production device identity.
Phase 4 checkpoint
- The passcode is attached to
authentication_policy_id, with username mode explicitly disabled. - The device group’s active policy assignment contains the certificate and authentication policy IDs.
- Registration used
x-passcodewithout an API token and includeddevice_group_id. - The response reports
REGISTEREDandENABLED, identifiesdevice_id, and contains one matching bootstrap certificate/private key. - Device and certificate inventory checks each return one exact record bound to
device_id. - The private key matches the certificate.
- The certificate path validates to
root-ca.pemthroughdevice-issuing-ca.pem, and its purpose is appropriate for the device workload.
You have validated the device enrollment and certificate path.
- If code signing is incomplete, configure and test private code signing.
- If both workstreams are validated, prepare the solution for production operations.