Architecture
1. What this is
A Windows credential provider that requires an SMS code in addition to the user's
existing Active Directory password, on four sign-in paths, with predictable exceptions
and a recovery route.
It is not an endpoint-security platform. It has no agent, no daemon, no tray icon, no
scheduled task, no auto-updater and no background uploader. There is no process running
on the machine between sign-ins.
2. The parts
┌─ LogonUI.exe (secure desktop) ──────────────────────────────────────┐
│ │
│ EnpointTrustCP.dll │
│ ├── EtProvider tiles, field layout, user array │
│ ├── EtCredential one tile; GetSerialization is the product │
│ ├── EtFilter provider visibility + remote credential │
│ │ routing │
│ └── worker threads every blocking call, off the UI thread │
│ │ │
└───────────┼─────────────────────────────────────────────────────────┘
│ statically linked
┌───────────▼──── et_core (portable, no Windows headers) ─────────────┐
│ jws documents policy identity scenario attempt grace │
│ state audit api engine log │
└───────────┬─────────────────────────────────────────────────────────┘
│ seams
┌───────────▼──── et_platform (Windows) ──────────────────────────────┐
│ BCrypt verifier · BCryptGenRandom · WinHTTP · DPAPI + DACL store │
│ session classifier · SID resolver · host inventory │
│ diagnostic log sink (ProgramData\EnpointTrust\Logs, JSON Lines) │
└───────────┬─────────────────────────────────────────────────────────┘
│ TLS 1.2+
┌───────────▼──── Core (OUT OF SCOPE — five operations) ──────────────┐
│ /v1/device-activations /v1/auth/validate /v1/auth/sms │
│ /v1/auth/verify /v1/audit/events │
└─────────────────────────────────────────────────────────────────────┘
etadmin.exe — one-shot, elevated, exits when done:
activate · refresh · status · pin · flush · repair · arm · disarm · purge
Local diagnostics follow the same split. et_core/log.h defines the levels, the record
shape, the field allowlist and the redaction; et_platform owns the file, its DACL,
rotation and the cross-process lock. The core's sink is optional and null by default, which
is a tested configuration — so the portable suite carries no logging dependency, and the
redaction is in code a test can drive. Default level trace, three files, about 15 MiB:
et_core has no Windows dependency on purpose. Every decision that matters — what a
signed document means, whether a grant may be spent, whether a denial still applies, what
coverage may honestly be claimed — is in code that a test can drive against a controllable
clock and an in-process transport. What is left in et_platform is the part a test
cannot replace, and it is deliberately thin.
3. The four responsibilities during a sign-in
- Validate.
POST /v1/auth/validatewith the account, the device, the observed
channel and operation, and a fresh nonce. Core answers require_sms,
allow_password_only or deny, signed and bound to this exact attempt.
- Challenge.
POST /v1/auth/smsstarts or replaces one challenge. The endpoint
never names a destination; Core selects the administrator-registered number and
returns only a mask.
- Verify.
POST /v1/auth/verifysubmits the typed code. Core answersallow_mfaor
a deny, again signed and bound.
- Report.
POST /v1/audit/eventssends what was actually observed, best effort.
Activation is a fifth operation, but it is setup plumbing, not part of a sign-in.
4. What holds the security properties
Windows authenticates; this provider decides whether to let it try. The credential
provider collects a password, packs it into a KERB_INTERACTIVE_UNLOCK_LOGON blob, and
hands it to LSA or does not. It never checks the password. A correct SMS code with a
wrong password still fails, at LSA, exactly as it would without this product installed.
The design is structurally incapable of making a bad password good.
Every authorization is a signed document, verified before it is read. Parse the three
JWS segments, require ES256 exactly, require a kid we hold, verify over the received
bytes, and only then decode the payload. Then check the bindings: tenant, device, user,
attempt, nonce, channel, operation, session, credential generation. A document that
verifies but is about a different attempt is worth nothing.
Release happens exactly once, and the debit comes first. AuthorizeRelease is the
single gate. For an offline grace authorization it takes a machine-wide mutex, re-reads
durable state, debits one submission, and persists — all before the caller may hand
anything to Windows. A crash after the debit does not refund it; two sign-ins racing on
one machine cannot both spend the last one.
One thing runs on the UI thread, deliberately: the final release gate. Everything else
that can block runs on a worker, and every apartment-bound interface pointer crosses to
that worker through the Global Interface Table, never by being AddRef'd and used directly.
GetSerialization runs on LogonUI's UI thread under an apartment-threaded provider, and
it calls Engine::AuthorizeRelease synchronously, because that call is what performs
the durable debit of an offline budget and re-checks for a denial learned since the
attempt started. Both have to happen after the user has committed and before the
credential reaches Windows. Doing the debit earlier would charge a user who then
cancelled; doing it on a worker would mean releasing a credential before the debit landed,
and a crash in that window is a submission spent twice.
What is bounded, and what is not. Two different mechanisms, and conflating them is how
the old blanket claim got written:
| What it is | Bound | |
|---|---|---|
| The engine lock | A std::timed_mutex serialising use of Engine | TryLockEngine(0) — taken without waiting. A worker inside the engine means the tile is not ready, and the callback returns "checking" rather than queueing behind a network call |
| The state-store lock | A machine-wide named mutex around the read-modify-write of durable state | lock_timeout_ms, 1500 ms on Windows. Failing to take it DENIES rather than proceeding unsynchronised |
That 1500 ms bounds the wait for the lock, not the disk I/O that follows it, and not
total callback latency. The work inside is a small read-modify-write of one file, but
WriteFile and a checked FlushFileBuffers on a sick volume have no deadline this code
imposes. Anyone citing a latency figure for GetSerialization should say that.
A failed gate never serialises a credential. AuthorizeRelease returning anything but
kOk means no release: the attempt is denied or left unauthorised, GetSerialization
returns CPGSR_NO_CREDENTIAL_NOT_FINISHED with a status message, and the single-use
BeginSubmission claim is what makes "released exactly once" structural rather than a
convention.
Not yet measured on Windows. Callback responsiveness under state-lock contention — two
sessions signing in at once — and under a storage failure. The 1500 ms figure is a chosen
bound, not an observed one.
A late answer cannot act. Each attempt owns a credential generation. Changing the
username or password bumps it, and a worker's result for an older generation is
discarded, not applied to whatever the tile holds now.
5. What it deliberately does not do
- It does not protect the NLA exchange. An RDP client's password reaches the target's
authentication stack before this provider sees anything. Target-side MFA is not MFA on
the NLA handshake, and supports_pre_nla_block is fixed false in the contract so no
configuration can claim otherwise.
- It does not observe every sign-in. Network logons, SMB, WinRM, scheduled tasks and
services never reach a credential provider. supports_complete_session_audit is fixed
false for the same reason.
- It does not resist a local administrator. The state directory's DACL and DPAPI machine
scope keep ordinary users out. Anyone with administrator or SYSTEM rights on the machine
is inside the accepted threat boundary, and so is anyone who can restore a disk image or
a VM snapshot.
- It does not enforce anything a configuration has not earned. If a usable alternate
provider is left visible, coverage is reported as alternate_paths_allowed. If a
provider we have never tested is registered, coverage is unknown — and it is not
suppressed to make the number look better.
6. Where to read next
| To understand | Read |
|---|---|
| The exact precedence rules and state transitions | state-and-precedence.md |
| What a credential provider is, from zero | credential-provider.md |
| What Core must do | core-integration.md |
| What is proven and what is not | coverage-matrix.md |
| How to install, recover and uninstall | installation.md |
| How to run the real acceptance matrix | windows-acceptance.md |