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
CreateNewfails; - 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
RunningandReady, 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.
- Create a test
C:\MinerOpsAgent\agent-config.jsoncontaining random Agent, Site, and Zone UUIDs plus placeholder secrets. - Place a harmless
MinerOpsAgentService.ps1in the target and registerMinerOpsAgentwith the exact installer-controlled SYSTEM/highest action:powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\MinerOpsAgent\MinerOpsAgentService.ps1". - Serve a loopback-only mock at
http://127.0.0.1:<port>. ItsPOST /api/agent/credential-transitionhandler records only the token hash and submittedagent_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. - Build a local package with that loopback origin and a random disposable 43-character token. Never download from or call a real Hub.
- 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:
- Malicious recovery, three invocations. Stop after
recovery-written, add a Users read/write ACE to recovery, replace its secret andoriginal_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. - 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. - 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. - 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-establishedmust have the same second-run result. - 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.
- 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. - 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. - Direct installer reset attempts. With a real external blocker present,
invoke
install.ps1 -ApproveFilesystemTrustResetAfterRebootdirectly 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.
Hard-link cases
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
ReplaceFilepartial 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.