# 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: ```powershell 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: ```powershell 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:`. 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: ```powershell $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: ```powershell 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. ## 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 `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.