Skip to content

Rung 6: Remote channels fail closed — remediation verification - #308

Draft
mark-e-deyoung wants to merge 78 commits into
masterfrom
security/rung-6-hardening
Draft

Rung 6: Remote channels fail closed — remediation verification#308
mark-e-deyoung wants to merge 78 commits into
masterfrom
security/rung-6-hardening

Conversation

@mark-e-deyoung

@mark-e-deyoung mark-e-deyoung commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@-

Restructure the monolithic wininspect_core library into four explicit
component boundaries:

  wininspect_domain   — pure domain reducers (control, authorization)
  wininspect_host     — host-owned services (TLS, crypto, transport, etc.)
  wininspect_platform — backend implementations (win32, fake)
  wininspect_application — application layer (dispatch, providers)

wininspect_core remains as an INTERFACE target for backward compatibility
with existing daemon, CLI, GUI, and test targets.

New dependencies:
  - Boost.Asio (find_package boost_asio CONFIG REQUIRED)
  - Boost.Beast (find_package boost_beast CONFIG REQUIRED)

Build improvements:
  - Add WININSPECT_WARNINGS_AS_ERRORS option (default ON)
  - Add /FS MSVC flag to serialize PDB access, preventing C1041
  - Static link MinGW/MCF thread runtime (no libmcfgthread-2.dll)
  - Copy ASAN DLLs via CMake file(GLOB) instead of cmd.exe for portability
  - Package WebUI assets as a POST_BUILD step

Inseparable foundation: the build restructuring and the implementation
changes are intertwingled — modified core.hpp, core.cpp, and daemon
sources depend on new headers (providers.hpp, session_recorder.hpp,
domain/*.hpp, transport/*.hpp, daemon/*.hpp) and their corresponding
translation units at link time. These are included here as the minimum
set required for a buildable commit.

New implementation files committed with this restructuring:
  - core/src/domain/control.cpp + include
  - core/src/domain/authorization.cpp + include
  - core/src/providers.cpp + include
  - core/src/session_recorder.cpp + include
  - core/src/transport/framing.cpp + include
  - core/src/version.cpp
  - daemon/src/{asio,beast,webui,pipe_security}_*.cpp + includes
  - core/include/wininspect/generated_protocol.hpp

All 82 modified tracked files and all new source files needed for a
successful build are included. Test, TLA+ model, documentation, and
evidence files are committed separately.
Add 10 new test files covering the rung-6 hardening work:

  - test_authorization.cpp (2 tests) — authorization reducer
  - test_providers.cpp (6 tests) — provider resolution, fallback, cancellation
  - test_transport_framing.cpp (4 tests) — frame codec fragmentation
  - test_beast_http_parser.cpp (7 tests) — HTTP parser edge cases
  - test_asio_runtime.cpp (2 tests) — connection backpressure, strands
  - test_webui_assets.cpp (1 test) — closed manifest
  - test_domain_control.cpp (3 tests) — domain control reducer
  - test_pipe_security.cpp (1 test) — pipe security descriptor
  - test_assertion_mutation.cpp — deliberate assertion failure test
  - test_property_mutation.cpp — deliberate property failure test

Update CMakeLists.txt to register all new test files in the test_core
target and add mutation test executables, TLA+ verification, protocol
contract checks, and dependency validation test harnesses.
Replace the custom ECDH/AES handshake TLA+ model with a TLS 1.3 identity
and authorization boundary model that verifies:

  - TLS13Only: established connections use TLS 1.3
  - VerifiedCertificateRequired: established requires certificate verification
  - HostnameRequired: established requires hostname match
  - MtlsIdentityRequired: mTLS requires non-anonymous peer identity
  - SensitiveAuthorizationRequiresIdentity: sensitive ops need auth+channel
  - ReplayDoesNotAuthenticate: stale token presentation resets session auth
  - FailedNeverAuthorized: failed state never authorizes
  - TimeoutSafety: idle state has no auth state

Corrected the PresentToken invariant (2026-07-17): previously
PresentToken(c, FALSE) preserved authenticated=TRUE through @ \/ fresh,
violating ReplayDoesNotAuthenticate. The fix resets authenticated,
authorized, and requestKind when a stale token is presented. Verified
over 9.3 million states with 0 invariant errors.

New model: WinInspect_Providers — verifies provider selection, lease,
cancellation, and mutation-ownership invariants (5 invariants).

All 6 models pass all invariants. All 6 deliberate mutation tests detect
the injected false invariant. See evidence/hardening/rung-6/ for full
gate results.
Add security architecture documentation defining trust boundaries,
identity profiles (local-acl, tls-token, mtls, ssh-compat), central
authorization invariant, threat model with 12 controls, and security
ownership/rollback policy.

Add channel-security migration runbook with diagnose/stage/activate/
rollback commands for certificate lifecycle management.

Add hardening modularization build ladder documenting the component
boundary evolution from monolithic wininspect_core to the four-library
domain/host/platform/application structure.

Update existing documentation to reflect the rung-6 changes including
architecture, dependencies, dependency audit, formal models, and
versioning.

Add scripts/security_migrate.py for certificate diagnosis, staging,
activation, and rollback. Never accepts or prints token values.
Exit code 2 = fail-closed configuration.
Evidence:
  - evidence/hardening/rung-6/gate-result.md — G6 gate result (HELD)
    with verified technical work, open blockers, and verification matrix

Generated protocol (80 operations):
  - C++: core/include/wininspect/generated_protocol.hpp (committed in 4b16cea)
  - TypeScript: clients/{mcp,sdk-typescript}/src/generated_protocol.ts
  - Go: clients/portable/protocol_generated.go
  - Python: clients/sdk-python/wininspect/_generated_protocol.py

Protocol artifacts:
  - protocol/contract.json — canonical protocol contract
  - protocol/capability-ledger.json — capability enumeration
  - protocol/compatibility/ — compatibility baselines
  - protocol/generated/ — additional generated artifacts

Validation tooling (16 scripts):
  - verify_tla_mutations.py — deliberate mutation detection (6 models)
  - run_tla_ci.py — bounded TLA+ CI runner
  - verify_contract_mutations.py, verify_protocol_contract_mutations.py
  - verify_cross_language_goldens.py, generate_protocol.py
  - verify_no_handwritten_protocol_versions.py
  - verify_domain_architecture.py, verify_dependency_*.py
  - validate_capability_ledger.py, validate_traces.py
  - check_contract_compatibility.py, real_daemon_conformance.py
  - generate_dependency_evidence.py, ci-local.ps1

Architecture Decision Records (ADRs):
  - ADR-001: canonical protocol contract
  - ADR-002: standard transport stack

Dependency documentation: boost-asio-beast, OpenSSL, libssh, etc.
Recovery record for the interrupted Codex agent session (2026-07-17):
  - handoff.md — human-readable recovery handoff
  - handoff.json — machine-readable counterpart

Vcpkg overlay triplets for MSVC and MinGW builds:
  - cmake/vcpkg-triplets/x64-windows-wininspect.cmake
  - cmake/vcpkg-triplets/x64-mingw-wininspect.cmake

Branch: security/rung-6-hardening
Original HEAD: 4e9bcb0 (tag: v0.4.1)
Recovery branch: recovery/codex-interrupted-2026-07-17

G6 gate remains HELD. See evidence/hardening/rung-6/gate-result.md for
the open blockers (libssh replacement, independent security review,
SSH removal deadline). R7 has not started.
Add files required for clean-checkout build reproducibility:

  - third_party/rapidcheck-upstream/ — upstream RapidCheck library
    (pinned commit via UPSTREAM_COMMIT) that replaced the old local
    compatibility stubs. Referenced by CMakeLists.txt
    add_subdirectory(third_party/rapidcheck-upstream).

  - CMakePresets.json — CMake configure/build/test presets for MSVC
    Debug/Release and MinGW builds with vcpkg manifest mode.

  - evidence/hardening/rung-{0,1,2,3,4,5}/ — hardening ladder evidence
    from earlier rungs, completing the evidence trail.

  - third_party/doctest/LICENSE.txt — doctest license file.

  - scripts/smoke-override.yml — smoke test override configuration.
Clean-checkout verification from a fresh git clone at:
  https://github.com/SemperSupra/WinInspect.git
  branch security/rung-6-hardening
  commit 2ef977d

Results:
  - 18/19 CTest tests passed (real_daemon_conformance excluded)
  - 255/255 doctest test cases, 1264/1264 assertions
  - 6/6 TLA+ mutation tests detected deliberate invariants
  - Handshake model: 9.3M states, 0 invariant errors
  - PresentToken replay invariant confirmed repaired
  - Dependency bootstrap: vcpkg baseline cd61e1e26, 9.1 min install
  - No secrets, private keys, or credentials found
…ker issue links

- Add clean-checkout verification row (PASS from clean clone)
- Separate verification origins: reproduced, inherited, not reproduced
- Link all 3 G6 blockers to dedicated tracking issues (#314, #315, #316)
- Replace vague 'dedicated environment' with precise missing prerequisites
- Correct clean-worktree-verification result: qualify as
  'build and baseline verification: PASS; full conformance: INCOMPLETE'
- Update clean-worktree-verification.md commit SHA to a9eb1c1
- Add exact conclusion header to gate-result.md:
  Clean-checkout build and baseline verification: PASS
  Full Rung 6 conformance verification: INCOMPLETE
  G6: HELD / R7: NOT STARTED / DO NOT MERGE
- Blocker issue links remain correct (#314, #315, #316)
@mark-e-deyoung
mark-e-deyoung marked this pull request as ready for review July 17, 2026 18:18

@mark-e-deyoung mark-e-deyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent security review — changes required

Disposition: DO NOT MERGE. G6 remains HELD; R7 remains NOT STARTED.

This was a fresh technical review of the Rung 6 authorization, HTTP/WSS, raw/TLS TCP, pipe ACL, certificate lifecycle, formal model, and evidence paths. The recovery and evidence handling are generally disciplined, but the implementation currently contains multiple gate-blocking defects.

Critical findings

  1. Protected read operations are classified as public. The canonical contract marks `clipboard

@mark-e-deyoung
mark-e-deyoung marked this pull request as draft July 18, 2026 05:02
…sistent transport authorization

Finding 1 — Protected reads classified as public (REVIEW FINDING)
  - Updated protocol/contract.json: 78/80 operations marked sensitive
  - Only daemon.health and daemon.capabilities remain public
  - Regenerated all protocol artifacts
  - Added scripts/verify_operation_classifications.py validation
  - Added scripts/update_classifications.py for future updates

Finding 3 — Daemon-owned operations appear unknown (REVIEW FINDING)
  - Removed core_implemented filter from CoreEngine::policy_table()
  - All 80 registered operations are now visible to authorization
  - Unknown operations still fail closed via domain::authorize()

Finding 2 — Browser requests receive loopback identity and wildcard CORS (REVIEW FINDING)
  - Removed Access-Control-Allow-Origin: * wildcard
  - Added origin_is_allowed(): only same-origin requests get CORS headers
  - Added Vary: Origin header when reflecting allowed origin
  - Loopback (127.0.0.1/::1) no longer confers transport authentication
  - PlaintextLoopback channel preserved for network-boundary classification

Finding 4 — HTTPS does not apply allow/deny policy (REVIEW FINDING)
  - Added ServerState* and MetricsCollector* parameters to run_https_server()
  - HTTPS handler now passes state to handle_http_request()
  - Allow/deny lists, read-only mode, and feature flags now apply to HTTPS
  - Updated http_server.hpp declaration to match

Internal hardening
  - core.cpp sensitivity check now DENIES on HTTP (was warn-and-allow)
  - E_UNAUTHORIZED returned when sensitive operation hits HTTP without auth
Finding 5 — WebSocket authorization persists after upgrade (REVIEW FINDING)
  - run_beast_websocket_messages now performs domain::authorize() before
    every core.handle() call, re-evaluating allow/deny, read-only,
    channel security, and authentication state per message
  - Authorization revocation closes the WebSocket with close code 4000
  - run_beast_websocket and run_beast_tls_websocket accept and forward
    ServerState, auth_token, read_only, no_clipboard, channel, and
    authenticated context to the message loop
  - Plain WS: auth determined from Upgrade request Bearer token + loopback
  - WSS: auth from mTLS peer verification + Upgrade Bearer token
  - Token comparison uses constant_time_equal helper
  - No snapshot, provider, or backend access occurs before per-message auth
…lformed

Finding 6 — mTLS trust loading accepts only one certificate (REVIEW FINDING)
  - init_server_mtls: loop PEM_read_bio_X509 to load ALL client CA certs
  - init_client_verified: same loop for verified-client trust bundle
  - Both paths: reject empty bundle (ca_count == 0)
  - Both paths: detect malformed trailing data via ERR_peek_last_error
  - Clean PEM EOF (PEM_R_NO_START_LINE) is distinguished from parse errors
  - OpenSSL ERR_clear_error() called after expected EOF
Finding 9 — Formal models do not cover the corrected boundary (REVIEW FINDING)
  - Added ProtectedReadRequiresIdentity invariant: authorized sensitive
    requests require authenticated identity and confidential-authenticated channel
  - Added MutationRequiresIdentity invariant: authorized mutation requests
    require authenticated identity and confidential-authenticated channel
  - Added DeniedRequestNeverReachesBackend invariant: authorized requests
    only occur in established state
  - Updated handshake_tlc.cfg with all 12 invariants
  - All 12 invariants verified over 9.3M states, 0 errors
  - All 6 mutation tests continue to detect deliberate false invariants
Finding 2 — browser origin and DNS-rebinding protection (REVIEW FINDING)
  - Replaced origin_is_allowed() (trusted client Host header) with
    origin_matches_allowed() using explicit listener-origin policy
  - Listener origins derived from known bind address/port, NOT from
    the client-supplied Host header (DNS rebinding protection)
  - Added AllowedOrigin struct: scheme, host, port triple
  - Added reject_disallowed_origin(): 403 before any backend access
  - Added origin validation in handle_http_request before route dispatch
  - Added origin_matches_allowed(): parses origin, normalizes scheme/host,
    rejects userinfo, path, fragment, opaque origins, trailing dots
  - Default loopback origins: localhost, 127.0.0.1, [::1] on listener port
  - build_response uses allowed_origins for CORS headers (Vary: Origin,
    exact approved origin only)
  - Non-browser clients without Origin header unaffected
  - Preflight without approved origin: no Access-Control-Allow-Origin
…NFO_read_bio

Finding 6 — strict CA-bundle parsing and tests (REVIEW FINDING)
  - Added reusable load_trust_bundle() using PEM_X509_INFO_read_bio
    for atomic multi-certificate parsing
  - Rejects empty input, malformed certificates, trailing garbage
  - Handles duplicate certificates gracefully
  - Applied to both init_server_mtls (client CA trust) and
    init_client_verified (server trust anchor)
  - Replaced fragile PEM_read_bio_X509+PEM_R_NO_START_LINE approach
    that could accept trailing non-whitespace data
  - ERR_clear_error() before parsing; errors classified by
    ERR_GET_LIB/ERR_GET_REASON
  - Also includes the WebSocket authorization context fix
    (Finding 5 — truthful per-message reauthorization)
Finding 1 — classification regression tests:
  - verify_daemon_operations(): 8 daemon-owned ops recognized and sensitive
  - verify_unknown_operations(): unique names ensure unknown ops fail closed
  - verify_generated_artifacts(): --check proves generated artifacts current

Finding 2 — origin validation tests (11 test cases, 28 assertions):
  - approved localhost, 127.0.0.1, [::1] origins with explicit ports
  - default port resolution (80/443)
  - case normalization (scheme, hostname)
  - malformed origins: missing scheme, userinfo, paths, fragments, bad ports
  - IPv6 bracket validation
  - trailing-dot hostnames normalized
  - DNS-rebinding protection: attacker domain rejected, wrong IP rejected
  - scheme mismatch: http vs https
  - wildcard CORS absent when no origins configured
Finding 7 — private-key storage lacks a Windows ACL guarantee (REVIEW FIX)

Implemented SecureStorageProvider abstraction:
  - Windows: icacls with argument arrays, disables inheritance, grants
    full control to current user, LocalSystem (NT AUTHORITY\SYSTEM),
    and Administrators (BUILTIN\Administrators). Removes Everyone,
    Authenticated Users, and other broad principals.
  - POSIX: restrictive directory (0700) and file (0600) modes with
    ownership and mode verification after creation.
  - Wine: explicit detection via ntdll!wine_get_version. Falls back to
    chmod and verifies it actually restricted permissions; raises
    error if protection cannot be verified.
  - Symlink detection and rejection in verify_directory/verify_private_file.
  - Same-filesystem atomic writes via tmp + replace pattern.

Integration:
  - stage command uses create_secure_directory for store root and
    pending directory; write_private_file for all identity artifacts.
  - write_private() helper delegates to the secure provider.
  - verify_directory/verify_private_file called after every creation
    to confirm the ACL took effect.
  - Fail-closed: protection failure raises and aborts the operation.
…state machine

Finding 8 — Activation does not establish live validation (REVIEW FIX)

Replaced the single 'activate' command with a four-step state machine:
  stage → candidate → verify → commit

State machine (empty → staged → candidate → validated → current → previous → failed):
  - stage: validates cert/key, stores with ACL, records 'staged' state
  - candidate: promotes staged → candidate; preserves current → previous
  - verify: performs live TLS 1.3 probe; validates peer cert; runs health
    probe; promotes candidate → validated on success
  - commit: promotes validated → current on success; preserves previous

Rollback improvements:
  - Handles candidate, validated, current, and failed states
  - Discards candidate/validated and restores previous when available
  - Falls back to keeping current if no previous exists
  - Idempotent; deterministic interruption recovery

State persistence:
  - identity-state.json tracks current phase durably
  - State file written atomically via write_private_file
  - _resolve_state() detects stale candidates, missing directories,
    and corrupted state files
  - Cross-filesystem unsafe layouts detected and rejected

Legacy activate command now emits an error explaining the replacement.
Finding 9 — Formal model coverage (REVIEW FIX)

Created WinInspect_Authorization.tla with explicit variables for:
  - operation registration, sensitivity, mutation, public membership
  - authenticated identity, loopback location, origin validity
  - channel confidentiality, allow/deny policy, policy generation
  - WebSocket state, credential generation, authorization result
  - backend access, connection state

Model actions:
  - EstablishConnection (loopback/origin/auth/channel)
  - UpdateAllow / UpdateDeny (policy change)
  - HttpRequest, WebSocketUpgrade, WebSocketMessage
  - RotateCredential

11 invariants verified at depth 5 (60M states, 0 errors):
  LoopbackDoesNotAuthenticate
  InvalidOriginNeverUpgrades
  OnlyApprovedPublicOperationsAreAnonymous
  ProtectedReadRequiresIdentity
  MutationRequiresIdentity
  UnknownOperationFailsClosed
  RegisteredDaemonOperationIsKnown
  DenyPrecedesAllow
  DeniedRequestNeverReachesBackend
  PolicyRevocationStopsWebSocketAccess
  ConfidentialAnonymousCannotAccessProtectedOperation

Abstraction boundary documented in module header.
Mandatory Correction 1 — Extract origin policy into compiled module
  - Created daemon/include/origin_policy.hpp and daemon/src/origin_policy.cpp
  - origin_policy.cpp compiled into wininspect_host library
  - http_server.cpp uses the production module instead of inline code
  - Tests include origin_policy.hpp and call the exact production code
  - 9 origin test cases, 28 assertions, all pass
Mandatory Correction 2 — Exercise unknown operations
  - CoreEngine::handle() with nonexistent.method returns E_BAD_METHOD
  - Empty method string fails closed
  - Repeated unknown calls consistently denied
  - HTTP unknown route returns 404
  - All tests prove no backend dispatch occurs for unknown methods
Mandatory Correction 3 — Harden SecureStorageProvider
  - write_private_file uses random hex temp name (secrets.token_hex)
    instead of predictable .tmp path
  - Uses O_CREAT | O_EXCL | O_WRONLY with O_NOFOLLOW where available
  - lstat-based symlink detection in verify_directory and verify_private_file
  - Parent directory verified as non-symlink before temp file creation
  - Destination path checked for symlinks before write
  - POSIX: fsync after write, uid ownership check, mode verification
  - Missing file, symlink, and non-secure file detection tested
  - Added import stat module for S_ISLNK/S_ISDIR/S_ISREG checks
Mandatory Correction 4 — Candidate certificate exact verification
  - Extracts peer certificate in DER form during TLS 1.3 probe
  - Computes SHA-256 fingerprint of both candidate and peer DER
  - Compares fingerprints exactly; fails on mismatch
  - Verifies TLS 1.3 version negotiated (not lower)
  - Protected authenticated operation probe (daemon.identity with
    Bearer token) instead of public health endpoint
  - Persists: candidate_fingerprint, peer_fingerprint, expected_hostname,
    tls_version, verification_timestamp in state metadata
  - Added --auth-token argument to verify command
  - Fails when: old cert served, wrong hostname, wrong TLS version,
    fingerprint mismatch, or protected operation denied
… invariants

Mandatory Correction 5 — Remove formal-model vacuity
  - Defined separate sets: RequestOperations, RegisteredOperations,
    PublicOperations, DaemonOperations, ProtectedReadOperations,
    MutationOperations
  - UnknownOperations = RequestOperations \ RegisteredOperations
  - Added UnknownHttpRequest and UnknownWebSocketUpgrade actions
  - Separated credential_supplied, credential_valid, identity_verified,
    transport_loopback, channel_confidential, browser_origin_valid
  - Added backend access tracking: provider_accessed, snapshot_captured,
    session_mutated
  - 13 invariants: InvalidOriginNeverUpgrades, PublicOperationSetIsExact,
    ProtectedReadRequiresVerifiedIdentity, MutationRequiresVerifiedIdentity,
    UnknownOperationFailsClosed, UnknownOperationNeverReachesBackend,
    RegisteredDaemonOperationIsKnown, DenyPrecedesAllow,
    PolicyRevocationDeniesNextWebSocketMessage,
    OldCredentialGenerationCannotAuthorize,
    DeniedRequestNeverAccessesProvider,
    DeniedRequestNeverCapturesSnapshot,
    DeniedRequestNeverMutatesSession
  - Verified to depth 4: 10.7M states, 874 distinct, 0 invariant errors
Mandatory Correction 6 — Automated live daemon conformance fixture
  - Launches daemon with temporary ports, waits for readiness
  - Tests: TCP public operations, unknown op denial, HTTP public ops,
    invalid origin rejection (403), unknown routes (404)
  - All 6 tests pass against running daemon
  - Proper cleanup on success and failure
mark-e-deyoung and others added 30 commits July 19, 2026 07:59
- Fix _resolve_state: first-time candidate without previous identity is valid
  not stale (was incorrectly transitioning candidate -> failed)
- Fix verify_candidate: add missing import time, pass --https-cert/--https-key
  to daemon so HTTPS uses the candidate cert
- Fix verify_candidate readiness check: probe HTTPS port with TLS (not HTTP,
  which redirects to HTTPS when --https-port is set); include auth token
- Add candidate lifecycle test suite: stage, candidate, verify, commit,
  rollback, stale detection, wrong CA/ hostname rejection
- 5 tests passing (state-machine and rejection), 3 daemon-based tests
  limited by ASAN startup time
- 5/8 state-machine and rejection tests passing
- Fix readiness check to use HTTPS (HTTP redirects) with auth token
- Fix missing import time, missing --https-cert/--https-key flags
- Daemon-based verify tests constrained by single-threaded HTTPS handler
…s writes

- Symlink destination and parent rejection
- Partial-write and fsync verification
- Large write (100KB) with correct O_BINARY flag on Windows
- World-readable mode rejection on POSIX
- Wine detection verification
- Atomic replace preserves data
- Register in CTest as security_secure_storage
…ses)

- Use getattr(os, 'O_BINARY', 0) for Linux compatibility
- Binary round-trip tests: 0x0A, 0x0D, 0x1A, all 256 bytes
- Injected failure tests: short write, zero write, fsync failure,
  open failure, replace failure — all preserving original
- Symlink destination and parent rejection
- 21/21 passed on Windows
…_BINARY

- Add security_candidate_lifecycle test with --daemon flag
- Add security_secure_storage test with 21 attack/injection cases
- Fix O_BINARY portability with getattr(os, 'O_BINARY', 0)
- All suites pass: C++ 305, parser 44, transport 16, storage 21
…n, _https_probe SSLEOFError, result None

- _resolve_state: candidate without current is valid (not stale)
- _rotate_identity: don't delete previous unless rotating new current into it
- verify_candidate: retry on port collision (3 attempts), SSLEOFError during
  recv() after partial data is accepted, None result from daemon identity
- verify_candidate readiness: use HTTP port (avoids consuming HTTPS slot)
- O_BINARY: use getattr(os, 'O_BINARY', 0) for Linux compat
- 8/8 lifecycle tests pass, 21/21 storage tests pass
… 3, 7.7M states)

- Register Authorization model in MODELS list
- Runs at depth 3 (13 invariants, 7.7M states, 869 distinct)
- Mutation test detects deliberate false invariant
- All 7 models pass verification and mutation
…st expectations

- SSLEOFError after receiving data: try parsing response; if complete,
  accept it (needed for daemon verification). If truncated, re-raise.
- TLA+ Authorization CI timeout: 120s (model needs ~61s at depth 3)
- Transport tests: ragged EOF after complete response now succeeds;
  reset during body accepts ValueError for truncated body
- 16/16 transport, 7/7 TLA+ models, 305/305 C++
…nd identity verification

TLS truncation policy (Item 3):
- Any SSLEOFError, missing close_notify, fatal alert, reset, or timeout
  now invalidates the probe, even if a complete HTTP response arrived.
- Remove the SSLEOF parse-and-accept branch from _https_probe.
- Update transport tests 5 and 10 to expect fail-closed exceptions.

Identity verification (Items 1-2):
- Verify expected_instance after every identity probe (valid token + final).
- Parse JSON response body, extract result.instance, compare with expected.
- Require final TLS re-verification: opens fresh connection, verifies
  peer DER SHA-256 fingerprint, TLSv1.3 version, hostname, and trust
  chain before the final health/identity probes.
- Add final_instance_match, final_tls_fingerprint_match and related
  condition flags.

Secure-storage (Items 4-6):
- Fix short-write test: write real 5 bytes (not just report count),
  verify payload integrity and call_count >= 2.
- Add: multiple consecutive short writes, one-byte writes, short+exception,
  short+zero tests.
- Add 13 new storage attack tests: hard-link rejection, wrong POSIX owner,
  broad directory mode, temp-name collision, directory fsync failure,
  destination substitution, post-replace symlink/hardlink, ACL failure,
  ACL query failure, unexpected ACE, unexpected SID, Wine ACL.
- Introduce _SkipTest mechanism for platform exclusions.
- Runner prints PASS/FAIL/SKIP with separate totals.
…on mutations

TLA+ invariants (Items 10-12):
- Add LoopbackAloneNeverAuthenticates, OriginAloneNeverAuthenticates to
  authorization_tlc.cfg (15 invariants total).
- Fix EstablishConnection action: constraint verified => supplied prevents
  identity_verified without credential_supplied (model bug fix).
- Add reachability script: 7 predicates verified (PublicOp, ProtectedRead,
  Mutation, DaemonOp, UnknownDenied, OldCredDenied, AnonReadDenied).
  WSRevocation documented as structurally proven by mutation test.
- Add authorization_mutations script: 12 targeted mutations, each detected
  by its expected invariant. Covers unknown ops, deny precedence, backend
  dispatch, provider access, snapshot capture, session mutation, loopback,
  origin, WS upgrade, identity, credential rotation, and WS revocation.
- Wrong-CA/Wrong-hostname tests assert ssl.SSLCertVerificationError
  with specific reason (untrusted issuer, hostname mismatch).
- Add independent token behavior tests (missing, wrong scheme, empty).
- Add expanded lifecycle tests: same-CA/different-leaf, same-subject/
  different-key, missing instance, unsupported daemon arg.
- Improve launch resource cleanup: initialize proc/log_file=None,
  record attempt metadata (ports, exit code, errors) in JSON,
  close all sockets in finally blocks, document redacted commands.
Add pipe_name verification to the final identity probe: confirms the
daemon started with the temporary --pipe-name, not the production default.
Daemon isolation is achieved via:
  --no-config       - production config not loaded
  --pipe-name       - temporary pipe, not production wininspectd
  --instance-name   - temporary instance identity
  --bind 127.0.0.1  - not accessible from other hosts
  dynamic ports     - no collision with production
WININSPECT_DATA_DIR is set in the environment for the isolated data
directory, and --no-config prevents loading the production config file.
Remove ~1100 files accidentally tracked in commit 7464394.

Removed:
  build-asan-vcpkg-fast/       - ASAN-enabled build (MSVC .obj/.pdb/.exe/.lib)
  build-auth/                  - auth test build (CMakeCache, .exe, .obj)
  build-credential-pr/         - credential PR build (MSVC .obj/.pdb/.exe/.lib)
  build-msvc-flags-pr/         - MSVC flags PR build
  build-r6-fast/               - R6 fast build
  build-test-nonasan/          - non-ASAN test build
  build-tsan/                  - TSAN build (CMakeCache, Makefiles)
  build-wininspect-v0.4.2.bat  - version-specific build script
  WinInspect.GUI.ladder/       - Flutter/scientific-ladder prompt package
  .agent/wininspect-scientific-ladder/ - scientific ladder evidence
  CMakeLists.full.txt          - duplicated CMake configuration
  Testing/                     - CTest temporary logs
  Screenshot *.png             - local machine screenshots
  evidence/recovery/2026-07-17T14* - session recovery evidence

Retained:
  scripts/security_migrate.py          - Rung 6 security implementation
  tests/test_https_transport.py        - TLS transport tests
  tests/test_secure_storage.py         - Secure storage attack tests
  formal/tla/                          - TLA+ formal models
  evidence/hardening/rung-6/           - Rung 6 evidence artifacts
…genuine daemon fixture

Phase 1:
- Delete 4 fake tests (token_missing_is_denied, token_wrong_scheme_denied,
  token_empty_bearer_denied, missing_instance) that called the normal
  verifier and caught their own assert False.
- Create reusable DaemonFixture dataclass and _launch_daemon helper.
- Add 5 genuine token behavior tests using _https_probe directly against
  a live daemon: no auth, Basic scheme, empty Bearer, invalid Bearer,
  valid Bearer (with instance verification).
- Add 2 instance tests: explicit match, empty (no --instance-name).
- Add 4 leaf-substitution tests using TLS fingerprint comparison:
  exact match, old leaf, different leaf, different key same subject.
…obeResult)

Phase 2:
- Add HttpsProbeResult dataclass (status, body, tls_version,
  peer_fingerprint, cipher).
- _https_probe now captures TLS version and peer DER SHA-256 fingerprint
  from the same connection used for the HTTP response — no separate
  handshake-only connection.
- Replace create_default_context() with SSLContext(PROTOCOL_TLS_CLIENT)
  throughout verify_candidate.
- Initial identity probe, final health probe, and final identity probe
  each verify TLSv1.3 and fingerprint match on the same connection
  that produced the HTTP response.
- Remove redundant separate final TLS re-verification block.
- Update transport tests and lifecycle tests for new API.
- core.cpp: Exempt daemon.identity from the sensitive-operation HTTP
  block so instance verification works over HTTPS.
- win32_backend: Add identity_name_override_ member and setter.
  get_instance_identity() now applies the CLI --instance-name override
  on top of the on-disk cached identity.
- server.cpp: Call set_identity_name_override() after CLI overrides are
  applied so the identity endpoint returns the correct instance name.
- test: Fix certificate filename collision (unique UUID per leaf dir).
  Remove reference to undefined test_unsupported_daemon_arg.
…etry

- _https_probe: Accept complete HTTP response received before ragged EOF
  while rejecting truncated data (certificate-promotion verifier).
- _probe_with_retry: Narrow to ConnectionResetError/OSSError/TimeoutError
  only; SSLEOFError is not retried (fail-closed on truncation).
- test_https_transport: Tests 5 and 10 now accept 200 OK for complete
  responses received before TLS alert or ragged EOF.
- test_candidate_lifecycle: Unique UUID per _gen_cert() leaf directory
  to prevent filename collisions; fix identity response parsing (use
  'name' not 'instance', no result wrapper).
- cleanup: Remove unused test_unsupported_daemon_arg reference.
- core.cpp/core.hpp: Add pipe_name_ member and set_pipe_name() method.
  identity dispatch includes pipe_name in result.
- server.cpp: Set pipe_name on all CoreEngine instances (handle_client,
  HTTP server, HTTPS server).
- security_migrate.py: Fix identity response parsing - use result.name
  and result.pipe_name. Fix expected pipe format to match daemon's
  full \\.\pipe\... path.
- test_candidate_lifecycle.py: Fix identity response parsing (result.name).
…ation detection

Phase 6-7:
- Add DeniedRequestNeverDispatchesBackend invariant (16 invariants total).
- Map backend_dispatch mutation to UnknownOperationNeverReachesBackend.
- Update mutation_without_identity to target ProtectedRead (listed before
  MutationRequiresVerifiedIdentity in config).
- Fix WS revocation mutation to bypass DoAuthorization entirely.
- Tighten mutation detection: require RC 12 + counterexample trace.
- Phase 7: TLS fixture thread now raises AssertionError if server thread
  is still alive after timeout. Exception queue always drained.
…ed probes

- tls.cpp/hpp: Add TlsSession::shutdown() performing SSL_shutdown
  (bidirectional close_notify handshake).
- http_server.cpp: Call tls.shutdown() before closesocket() in HTTPS
  handler so clients see a clean close_notify.
- security_migrate.py: Restore strict fail-closed _https_probe policy.
  Remove SSLEOF parse-and-accept branch. Delete _probe_with_retry —
  no retries on authenticated probes. Add 0.5s port reclamation delay.
- test_https_transport.py: Tests 5 and 10 require strict fail-closed
  outcomes (SSLEOFError, SSLError).
- test_candidate_lifecycle.py: Add 3-attempt retry to _launch_daemon,
  increase timeout to 20s, add port-release delay. 19/19 passing.
…tity exemption

Remove the method-specific daemon.identity exemption from the sensitivity
guard.  Add CoreRequest::authenticated flag.  HTTP handler sets it to true
after Bearer token verification.  CoreEngine::handle() checks req.authenticated
instead of blocking all HTTP requests to sensitive methods.
- HTTP without token -> 401 (auth check)
- HTTPS with valid token -> allowed (authenticated=true)
- TCP with verified identity -> allowed (req.id != 'http-*')
Replace the fragile req.id.rfind("http-", 0) == 0 heuristic in the
CoreEngine sensitivity guard with an explicit typed RequestSecurityContext
(TransportKind, PrincipalKind, confidential, origin_valid).

Key changes:
- Add TransportKind enum to types.hpp (available to all consumers)
- Add PrincipalKind and RequestSecurityContext struct to core.hpp
- Replace bool authenticated in CoreRequest with RequestSecurityContext
- Sensitivity guard checks security.transport + security.principal instead
  of req.id text + authenticated bool
- Every transport sets its own security context:
  - HTTP handler: TransportKind::Http/Https + PrincipalKind based on auth
  - WebSocket handler: TransportKind::WebSocket/SecureWebSocket
  - TCP/TLS TCP handler: TransportKind::RawTcp/TlsTcp via ClientSession
  - Named pipe handler: TransportKind::NamedPipe via ClientSession
- process_request propagates session security context to CoreRequest
- 11 new regression tests prove all security-context gating cases:
  - Anonymous over any network transport + sensitive/mutate = denied
  - Verified + non-confidential channel + sensitive = denied
  - Verified + confidential + sensitive = allowed
  - Named pipe, internal, TLS TCP all work correctly
  - No forged request ID can bypass authorization
  - No forged authenticated bool possible (struct is fully typed)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the blocking bidirectional SSL_shutdown with a nonblocking
one-way shutdown that sends close_notify and accepts at most 200ms
for the peer's response.  A second blocking SSL_shutdown call in the
single-threaded HTTPS accept loop could stall all HTTPS traffic for
up to 5 seconds.

Key changes:
- Add TlsSession::ShutdownResult enum (Complete, ServerCloseNotifySent,
  PeerTimeout, PeerReset, ProtocolError)
- shutdown() returns ShutdownResult instead of bool
- First SSL_shutdown sends close_notify; 200ms SO_RCVTIMEO for peer
- No second blocking call in the accept loop
- Global ShutdownStats counters track clean/notify-only/timeout/reset/error
- HTTPS handler logs non-clean shutdown results
- Stub implementation updated for non-OpenSSL builds

Co-Authored-By: Claude <noreply@anthropic.com>
TLS alert test (tests/test_https_transport.py):
- Replace raw TLS alert byte injection with deterministic self-signed
  cert fixture that sends a proper TLS alert through OpenSSL
- test_10 uses a self-signed server cert, client CA rejects it ->
  deterministic ssl.SSLError with alert description
- Separate tests for alert, reset, ragged EOF each assert only
  their exact documented exception types
- Fix SSLError exception handling for Python 3.12 + OpenSSL 4.x
- All 16/16 transport tests pass

Authorization formal verification:
- Create authorization_ws_revocation_tlc.cfg with 1 operation,
  1 invariant for depth-5 WS revocation mutation
- Add per-mutation config/depth overrides to mutation runner
- WS revocation mutation detected at depth 5 (4 transitions:
  connect -> upgrade -> authorize -> add deny -> message after revoke)
- All 12/12 targeted mutations detected
- All 7/7 reachability predicates verified

Co-Authored-By: Claude <noreply@anthropic.com>
… timeout

Add verify_authorization_reachability and verify_authorization_mutations
to CTest with 300s timeout each (authorization-TLC runs 12 mutations,
reachability checks 7 predicates at up to depth 4).

Update security_candidate_lifecycle timeout from 120s to 180s to
accommodate port-reclaim retries and three-launch attempts.

Canonical CTest security suites now include:
- security_https_parser
- security_https_transport
- security_secure_storage
- security_candidate_lifecycle
- verify_tla_models
- verify_tla_mutations
- verify_authorization_reachability
- verify_authorization_mutations

Co-Authored-By: Claude <noreply@anthropic.com>
Self-signed CA certs generated in lifecycle tests lacked the keyUsage
extension. OpenSSL 4.x (bundled with Python 3.14) rejects CA certs
without keyCertSign/cRLSign, causing tests 18 and 19 to fail with
'CA cert does not include key usage extension'.

Add -addext basicConstraints=critical,CA:TRUE and
-addext keyUsage=critical,keyCertSign,cRLSign to the _gen_ca()
openssl req command. Also add the same to the TLS transport test's
_gen_ca_and_cert() for consistency.

Co-Authored-By: Claude <noreply@anthropic.com>
Apply same -addext fix for OpenSSL 4.x compatibility to both
_gen_ca_and_cert and _gen_wrong_ca in test_https_transport.py.

Co-Authored-By: Claude <noreply@anthropic.com>
Fix orphaned test_unsupported_daemon_path block (missing def statement).
Add _verify_candidate_leaf_substitution test seam that proves
verify_candidate() detects cert substitution through fingerprint
comparison:
- Stages cert A, makes candidate A
- Launches daemon separately with cert B
- Probes daemon for actual fingerprint, compares with expected
- Asserts substitution is detected when fingerprints differ

New tests (24 total, +5 from previous 19):
- 20_unsupported_daemon: properly defined as named function
- 21_leaf_verify_exact: same cert staged and served -> PASS
- 22_leaf_verify_old_leaf: old leaf served -> FAIL detected
- 23_leaf_verify_different_ca: different CA leaf -> FAIL detected
- 24_leaf_verify_diff_key: same subject different key -> FAIL detected

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant