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:

diagnostics.md.

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

  1. Validate. POST /v1/auth/validate with 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.

  1. Challenge. POST /v1/auth/sms starts or replaces one challenge. The endpoint

never names a destination; Core selects the administrator-registered number and

returns only a mask.

  1. Verify. POST /v1/auth/verify submits the typed code. Core answers allow_mfa or

a deny, again signed and bound.

  1. Report. POST /v1/audit/events sends 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 isBound
The engine lockA std::timed_mutex serialising use of EngineTryLockEngine(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 lockA machine-wide named mutex around the read-modify-write of durable statelock_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

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.

services never reach a credential provider. supports_complete_session_audit is fixed

false for the same reason.

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.

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 understandRead
The exact precedence rules and state transitionsstate-and-precedence.md
What a credential provider is, from zerocredential-provider.md
What Core must docore-integration.md
What is proven and what is notcoverage-matrix.md
How to install, recover and uninstallinstallation.md
How to run the real acceptance matrixwindows-acceptance.md