Skip to content

The device credential, claim by claim ​

WIN-DLP-EPIC-003 names five things: a one-time token, a device-bound key pair, a server-issued scoped credential, rotation, and revocation. Its reason is the one to keep in mind while reading: the device credential is what makes an event attributable, and a weak enrolment makes every incident arguable.

Four of the five are built and now tested. The fifth is half-built, and the half that exists is the half nobody can reach.


1. The one-time token ​

Issued from the console, held only as a hash, and validated in a single SQL predicate: not revoked, not expired, uses remaining, right tenant.

ClaimProved by
A token enrols exactly oncetests/enrollment_lifecycle.sh §1, and tests/e2e_vertical_slice.sh
An unknown token is refusedtests/enrollment_lifecycle.sh §2
A revoked token is refusedas above
An expired token is refusedas above
All four refusals say the same thingas above - asserted by comparing the messages, not by reading them

That last row is the one worth having. The uniformity was a deliberate choice in enrollment.rs and was previously only a comment; an error that distinguished expired from revoked from never existed would let anyone holding a list of guesses learn which ones had once been real.

The refusals are probed through the agent binary, not with a hand-rolled HTTP body. Enrolment carries a real PKCS#10 CSR and the public key the endpoint actually holds (Ed25519 for software storage or ECDSA P-256 for TPM), and a shell script forging that body would be testing its own JSON - and would go on passing after the contract changed underneath it.

2. The device-bound key pair ​

Generated on the endpoint. The private key never leaves it, and the server never sees anything but the public half.

ClaimProved by
A verified Platform Crypto Provider key is selected as TPM-backed; an unavailable provider falls back; an unusable existing key is never overwrittendevice_signer::tests::provider_selection_falls_back_only_when_no_existing_key_is_at_risk plus the Windows CNG path
The TPM key name is deterministic and an existing verified key is reopened, not replaceddevice_signer::tests::the_persisted_key_name_is_stable_for_the_same_machine; NCryptCreatePersistedKey/NTE_EXISTS path
The TPM P-256 CSR proves possession of the exact enrolled keydevice_signer::tests::tpm_csr_proves_possession_of_the_enrolled_p256_key
The key is wrapped by the platform keystore - DPAPI machine scope on Windows, a restricted file on Unixkeystore::tests::protection_round_trips_on_every_platform
An existing software/DPAPI identity keeps its key across an ordinary upgradeidentity::tests::an_existing_software_identity_is_not_silently_replaced_by_tpm
A restored TPM identity with no usable reference fails closedidentity::tests::a_restored_tpm_identity_without_its_platform_key_reference_fails_closed
An identity wrapped on another platform fails loudly rather than silently re-enrollingkeystore::tests::an_identity_from_another_platform_fails_loudly
A keystore failure is never the expected statekeystore::tests::a_keystore_failure_is_not_the_expected_state_anywhere
The identity file is not world-readableidentity::tests::the_identity_file_is_not_world_readable, and tests/e2e_vertical_slice.sh checks the mode is 600 on a real enrolment
A corrupt identity is an error, not a silent re-enrolmentidentity::tests::a_corrupt_identity_is_an_error_not_a_silent_reenrollment
A missing identity is "not enrolled", not an erroridentity::tests::a_missing_identity_is_none_not_an_error
The hardware id is stable and opaqueidentity::tests::hardware_id_is_stable_and_opaque

The pair at the end is the interesting one: corrupt and absent must lead to different places. An agent that re-enrolled itself because a file would not parse would quietly abandon the device record every incident is attributed to.

3. The scoped credential ​

A client certificate issued by a per-tenant device CA, its subject encoding the tenant and the device.

ClaimProved by
A certificate presented for another device is refused, even in observe modemtls::tests::a_certificate_for_another_device_is_refused_even_while_observing
The three modes - off, observe, required - behave as a table rather than as a chain of conditionsmtls tests, 14 of them over the 12-cell table
Every agent request is signed over a canonical string including the body hash, using the algorithm pinned at enrollmentdlp-crypto request_auth tests for Ed25519 and ECDSA P-256
A replayed nonce is refusedtests/e2e_vertical_slice.sh §15

mTLS is Off by default. The certificate is issued and stored from the first enrolment regardless, so turning it on is a configuration change rather than a re-enrolment of the fleet.

4. Revocation ​

ClaimProved by
The console can revoke a device, and a reason is requiredtests/enrollment_lifecycle.sh §4
A revoked device can no longer check inas above
The device's own row records itas above
The revocation reaches the audit logas above

Before this audit, none of that was tested. authenticate_agent gates on status = 'active' and nothing exercised the clause, so a regression that dropped it would have passed every test in the repository while leaving every revoked endpoint able to report events as itself - which is exactly the "arguable incident" the epic is written against.

5. Rotation - built on the server, unreachable from the agent ​

POST /api/v1/agent/certificate works: it issues a fresh certificate from the tenant's device CA, updates the row, and writes an agent.certificate_rotated audit entry.

Nothing calls it. There is no request to that path anywhere in agent/core. The agent keeps the certificate it was issued at enrolment until it expires, and in Required mode that is the day the endpoint goes quiet.

device_cert_days is configuration, so somebody will set it to 90 and every endpoint in the fleet will lose its credential on the same day - three months after deployment, which is also the day the fleet is least able to say why.

Filed as DLP-261, with the shape it needs: rotate on a fraction of the lifetime remaining rather than on an expiry date, sign the request with the credential being rotated, keep the old certificate until the new one has worked once, and never let a failed rotation be fatal. An endpoint that bricked its own credential trying to renew it would be worse than one that expired.


6. What is not covered ​

  • No unit tests in enrollment.rs. Every claim above about the server side is proved by a shell test against a real server and a real database. That is the right level for SQL predicates and issuance, and it does mean the module has no test that runs in cargo test.
  • Re-enrolment of a known machine - the legacy_hardware_id migration path - is covered by tests/hardware_id_migration.sh and is not re-audited here.
  • Uninstall (POST /api/v1/agent/uninstall) is a different lifecycle event and belongs with DLP-154, not here.
  • CI has not run it. tests/enrollment_lifecycle.sh is wired into the e2e job, and no GitHub-hosted job ran from 2026-09-15 to 2026-10-01 (DLP-1215), and none of the Windows jobs has passed since (Windows CI status); the suite has run where a session ran it by hand.

Statements here describe what is built and tested; planned work is marked as planned.