Files
minerops/agents/windows/tests/WINDOWS_CREDENTIAL_TRANSITION_VALIDATION.md
T

18 KiB

Disposable Windows filesystem-trust validation

Native Windows evidence is required before deployment. The cross-platform behavioral harness proves deterministic state-machine decisions; it cannot prove NTFS access checks, directory-handle sharing, junction behavior, same-directory rename semantics, or Scheduled Task behavior.

Isolation

Use a newly created, disposable Windows 10/11 or Windows Server VM. Do not join it to a production domain or connect it to a production MinerOps Hub, database, or miner network. Never use a production package, URL, Agent ID, transition token, shared secret, or API3 credential. Copy only the candidate agents\windows directory to the VM and take a snapshot first.

Run 64-bit Windows PowerShell 5.1 as Administrator:

Set-ExecutionPolicy -Scope Process Bypass
.\tests\Test-MinerOpsCredentialTransition.Windows.ps1 `
    -AcknowledgeDisposableVm

Record the exact pass count printed by the candidate harness. Do not claim the native suite passed unless this command actually completed on Windows. Save the complete transcript plus:

Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsBuildNumber
$PSVersionTable

The native primitive suite verifies:

  • protected credential, enrollment, installer, and trust-blocker files are created with their final FileSecurity, held exclusively, and read back through the same still-empty handle before any content is written;
  • the directory and credential-file DACLs deny standard-user read, create, delete, rename, and replacement-swap operations;
  • ACL setup or read-back failure cannot leave secret-bearing content;
  • a pre-created pathname remains unchanged when CreateNew fails;
  • an insecure artifact is repaired through an exclusive handle with explicit security rights, then rejected; inability to acquire those rights fails closed without a path-based fallback;
  • the durable blocker exists before an ACL-repaired artifact could be reconsidered, and remains effective on the next check;
  • quarantine evidence is unreadable to the standard user;
  • the two-rename install leaves the new active file and a non-authoritative prior remnant, and an active-missing install does not invent prior authority;
  • a directory junction is rejected;
  • a permissive directory is hardened, gets an external durable blocker, and cannot continue in that process or an ordinary rerun;
  • pre-repair standard-user file and directory handles are exercised after ACL repair while automatic transition remains blocked regardless of whether the handle operation succeeds;
  • hard-linked enrollment, runtime, and backup targets are rejected; and
  • a real harmless expected Scheduled Task is positively verified for exact Running and Ready, while missing, query failure, disabled, malformed, wrong-action, and wrong-principal cases fail closed.

Full-kernel-boot epoch matrix

The source uses the canonical value windows-full-boot-event-v1:<16 lowercase hex digits>. The payload is the Event Log RecordId anchor of the newest System-log event from Microsoft-Windows-Kernel-Boot, event ID 27, whose namespace-aware XML payload contains exactly one canonical BootType value equal to 0. ProviderName, Id, nullable RecordId, and nullable Version must have the real typed EventLogRecord shapes; supported versions are 0 and 1. The record ID is not an immutable boot GUID. A reset requires a valid current anchor strictly greater than the stored anchor. Equal, lower/restored/reused, missing, and ambiguous anchors fail closed. The implementation deliberately does not use wall-clock time or the Windows BootId counter: Fast Startup is a hibernated kernel restoration and must not authorize reset merely because Windows reports a new startup sequence.

On a disposable VM, dot-source the transition script and record Get-MinerOpsBootIdentifier before and after every row. Preserve the raw event object and ToXml() output from Get-WinEvent. For each event, record the runtime types and values of ProviderName, Id, RecordId, and Version, the root namespace, and the System/EventData/Data[@Name='BootType'] shape.

Transition Required canonical identity
Change system clock within the running session unchanged
Change timezone within the running session unchanged
Sleep and resume unchanged
Hibernate and resume, where supported unchanged
Shutdown and power-on with Fast Startup, where supported unchanged
Restart from the Windows power menu strictly greater record anchor

For the Fast Startup row, independently confirm that Windows restored kernel session 0 and that the latest event 27 has a nonzero BootType; the newest full-boot (BootType=0) event record must remain the same. For the Restart row, confirm a new exact BootType=0 event and a new canonical identity. Also test a missing System log/provider event, duplicated BootType, malformed XML value, wrong namespace, unsupported/null version, wrong provider/event ID, zero/malformed record ID, and query access failure; every case must fail closed.

Use a separate disposable snapshot for log-retention behavior. Preserve the pre-clear stored anchor and evidence first, then clear only the disposable VM's System log. Verify that missing history cannot authorize reset. After a full Restart, if Event Log numbering restarts at or below the stored anchor, verify that automatic reset remains blocked and requires operator intervention. Also restore a snapshot exposing only an older valid full-boot event and confirm the lower record ID is rejected. Do not add a timestamp fallback for any row. A native result is required because EventLogRecord property types, provider namespace/version, log-clear numbering, and power-state behavior cannot be proven by the Linux harness.

Loopback workflow fixture

Use a fresh snapshot for full workflow testing. All values must be disposable.

  1. Create a test C:\MinerOpsAgent\agent-config.json containing random Agent, Site, and Zone UUIDs plus placeholder secrets.
  2. Place a harmless MinerOpsAgentService.ps1 in the target and register MinerOpsAgent with the exact installer-controlled SYSTEM/highest action: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\MinerOpsAgent\MinerOpsAgentService.ps1".
  3. Serve a loopback-only mock at http://127.0.0.1:<port>. Its POST /api/agent/credential-transition handler records only the token hash and submitted agent_secret_hash, returns the fixed disposable Agent/Site/Zone identity, returns the same response for the same token and hash, and rejects a changed hash.
  4. Build a local package with that loopback origin and a random disposable 43-character token. Never download from or call a real Hub.
  5. For each case, restore the snapshot or recreate the fixture. Run once with the task initially running and once with it intentionally stopped.

The child-process shape for hard termination is:

$checkpoint = 'pending-written'
$code = @"
. 'C:\validation\agents\windows\Upgrade-MinerOpsAgentCredential.ps1'
Invoke-MinerOpsAgentCredentialTransition `
  -TargetRoot 'C:\MinerOpsAgent' `
  -PackageRoot 'C:\validation\agents\windows' `
  -ScheduledTaskName 'MinerOpsAgent' `
  -CheckpointAction { param(`$name) if (`$name -eq '$checkpoint') { Stop-Process -Id `$PID -Force } }
"@
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($code))
Start-Process powershell.exe `
    -ArgumentList '-NoProfile','-EncodedCommand',$encoded -Wait

After termination, rerun normally without deleting, repairing, or editing any artifact. Every retry request must use the same token and agent_secret_hash; no retry may generate a replacement secret.

Hard-termination matrix

Checkpoint State before rerun Required rerun result
directory-repair-blocker-written external protected blocker durable before repair no artifact is trusted; every ordinary invocation remains blocked
directory-trust-established hardened directory; external blocker retained pre-repair recovery/pending/remnants are quarantined without parsing; even a clean repair cannot continue in this process
directory-containment-scan-complete initial known-name scan finished; external blocker retained a late recovery/pending file never becomes authority on this or later ordinary invocations
trust-reset-authorized changed full-boot epoch and strict original task intent are durable; blocker retained approved retry preserves that intent and repeats publication; it cannot delete early
trust-reset-runtime-published fresh package runtime is durable; blocker is runtime-published approved retry resumes credentials/config/task convergence with the blocker retained
quarantine-blocker-written blocker durable; insecure authoritative artifact may remain second run never parses or redeems it
artifact-moved-to-quarantine authority name absent; evidence is beneath quarantine evidence remains non-authoritative and the quarantine directory is reverified
quarantine-directory-verified evidence is under the protected non-traversable quarantine directory rerun remains blocked and never reads or ACL-repairs evidence contents
recovery-written protected recovery; original task state retained exact saved token/secret is reused
task-stopped recovery protected; task stopped; no request yet transition completes and restores original task intent
server-committed server may have committed; task stopped idempotent same-hash redemption completes
pending-written protected matching recovery and pending pending is validated and reused, not overwritten
before-replace active legacy config plus pending first rename may proceed; rerun converges
active-moved-to-prior active missing; matching pending and legacy prior present prior is never promoted; pending becomes active
pending-moved-to-active committed active plus prior; pending absent active is verified; prior is cleanup-only
after-replace same as prior row active is verified before task restoration
task-restored active and final task state verified retry verifies again and cleans up
before-trust-blocker-removal active config and final strict task state verified; external blocker still present only an explicitly approved parent retry may verify again and remove the blocker
before-recovery-cleanup active/task verified; recovery remains last retry uses recovery and completes cleanup

For each row inspect ACLs and task state without displaying secret contents:

Get-Acl C:\MinerOpsAgent | Format-List
Get-Acl C:\MinerOpsAgent\agent-credential-transition.recovery.json `
    -ErrorAction SilentlyContinue | Format-List
Get-Acl C:\MinerOpsAgent\agent-config.json.pending `
    -ErrorAction SilentlyContinue | Format-List
Get-ScheduledTask -TaskName MinerOpsAgent | Select-Object TaskName, State

Provenance and quarantine cases

Run these as separate restored-snapshot cases:

  1. Malicious recovery, three invocations. Stop after recovery-written, add a Users read/write ACE to recovery, replace its secret and original_task_running, and run twice. The first run must create a blocker, quarantine without parsing, and send no request. The second must remain blocked, send no request, keep the real original task state, and omit the attacker secret from all output. A third ordinary run must remain blocked.
  2. ACL-repair laundering attempt. Terminate at quarantine-blocker-written, then repair the still-authoritative recovery file ACL by hand without deleting the blocker. The next run must quarantine it without reading or further ACL mutation and must not redeem it. This proves ACL correctness cannot override the durable provenance decision.
  3. Malicious pending, three invocations. Stop after pending-written, add a Users ACE and alter the credential/Agent data. All runs must send no new request and must never promote pending.
  4. Permissive directory with attacker state. Add Users Modify to the target directory, place recovery and pending before repair, and run. Both artifacts must be quarantined, a blocker retained, and no request sent. A hard stop at directory-trust-established must have the same second-run result.
  5. Quarantine preservation. A later successful, separately authorized transition must not silently delete unresolved blocker or evidence files. Operator review must remove them only after invalidating the exposed token and obtaining a new package.
  6. Late arrival after scan. At directory-containment-scan-complete, use a handle opened before repair to attempt creation/rename of recovery and pending names, including a file whose ACL now looks correct. The current, second, and third ordinary runs must send no request and perform no activation regardless of the handle operation's Windows result.
  7. Trust-epoch reset. Use Restart, confirm the canonical full-kernel-boot event identity changed, clear suspicious transition state only under the approved procedure, and run with -ApproveFilesystemTrustResetAfterReboot. Confirm fresh runtime publication occurs while the external blocker exists, old runtime is never a rollback source, the task remains stopped until transition verification, and the blocker remains through credential/config convergence and final strict task verification. It may disappear only after all of those parent checks pass. The same switch before Restart must fail. Repeat after clock/timezone change, sleep, hibernate, and Fast Startup; each must still fail as the same epoch.
  8. Direct installer reset attempts. With a real external blocker present, invoke install.ps1 -ApproveFilesystemTrustResetAfterReboot directly for a successful publication and an injected publication failure. Repeat with the task disabled, Unknown, Queued, wrong action, and wrong principal. The blocker must remain in every case. Then run the complete parent workflow and prove deletion occurs only after its final convergence checks.

In every quarantine case, test standard-user reads against every minerops-quarantine-credential-transition-*.evidence file. Access must be denied, and neither console nor error transcripts may contain its contents.

On NTFS, create a second name for each candidate with New-Item -ItemType HardLink. Test attacker-created enrollment pending, recovery, and final config paths; installed runtime destinations; and rollback sources/destinations. Every case must fail before secret write, redemption, publication, or restore. Verify that no attacker-readable alias contains a permanent credential and that a failed CreateNew does not delete or adopt the pre-existing file.

Strict Scheduled Task matrix

Use the exact installer-controlled action and principal as the valid fixture. Run both durable intents through Running, Ready, missing, provider/query exception, access denied where practical, null/empty state, Disabled, Unknown, Queued, malformed object/definition, multiple results, wrong executable/script arguments, and wrong principal. Only running plus Running and stopped plus Ready succeed. Stopped plus Running must issue stop and then positively verify Ready.

Capture the actual provider types for TaskPath, State, Settings.Enabled, Actions, action Execute/Arguments, and principal UserId, LogonType, and RunLevel. Verify root TaskPath is an actual string equal to \ and Enabled is an actual Boolean $true. Missing, null, empty, array, numeric, and truthy string substitutions must fail; provider enum normalization is allowed only for State, LogonType, and RunLevel.

After the server-commit checkpoint, inject final task verification failure. Recovery must remain and no success output or cleanup may occur. Repair the task and rerun; the recorded token and agent_secret_hash must be identical and the retry must converge without generating a new secret.

Active-missing and replacement cases

Run all of these with a cryptographically matching protected recovery record:

  • active missing, protected matching pending present, no prior (legacy ReplaceFile partial state): converges with the same token and secret;
  • active missing, matching pending and valid legacy prior present (new first rename completed): converges through pending; prior is never authority;
  • committed active and valid prior present, pending absent (new second rename completed): verifies active and cleans prior last;
  • failure before the first rename: active and pending remain, rerun converges;
  • failure between the two renames: active is absent, pending and prior remain, rerun converges;
  • failure immediately after pending becomes active: active and prior remain, rerun verifies active before cleanup;
  • active missing with no recovery or with unknown pending: fails closed;
  • active missing with malformed pending: fails before redemption;
  • active missing with mismatched Agent, secret, or secret hash: fails before redemption; and
  • active missing with only a stale prior: fails closed and never promotes it.

For every successful active-missing case, confirm exact Agent/Hub/Site/Zone identity, the same token and secret hash on every request, active-file DACL, and final Scheduled Task intent. Initially running must end running; intentionally stopped must remain stopped.

Completion and cleanup

At successful termination, recovery, pending, prior, and consumed enrollment files are absent; unresolved quarantine/blocker evidence is never silently removed. Console/error transcripts contain neither token nor generated secret.

Record any disposable account, Scheduled Task, junction, directory, or mock service left by an interrupted validation, remove only those validation resources, then restore the snapshot or destroy the VM.