306 lines
18 KiB
Markdown
306 lines
18 KiB
Markdown
# 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:<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:
|
|
|
|
```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.
|