diff --git a/sso-mfa/k8s/keycape/keycape-client.py b/sso-mfa/k8s/keycape/keycape-client.py index 264f6d9..08be177 100644 --- a/sso-mfa/k8s/keycape/keycape-client.py +++ b/sso-mfa/k8s/keycape/keycape-client.py @@ -200,10 +200,10 @@ def deployment_bound(deployment, client): def check_config(binary, config_text, env_names, policy_store): """KeyCape's startup checks on one registry: names and messages only, no values.""" - command = [binary, 'check-config', '--config', '/dev/stdin', '--env-names', ','.join(env_names)] + command = [str(Path(binary).resolve()), 'check-config', '--config', '/dev/stdin', '--env-names', ','.join(env_names)] if policy_store: command.append('--policy-store') - result = subprocess.run(command, input=config_text.encode(), capture_output=True, timeout=60) + result = subprocess.run(command, input=config_text.encode(), capture_output=True, timeout=60, env={}) report = json.loads(result.stdout) require(isinstance(report, dict) and isinstance(report.get('valid'), bool) and report['valid'] == (result.returncode == 0), 'check_config_output_shape') diff --git a/sso-mfa/k8s/keycape/registration-adapter.md b/sso-mfa/k8s/keycape/registration-adapter.md index 1ace794..148e6c3 100644 --- a/sso-mfa/k8s/keycape/registration-adapter.md +++ b/sso-mfa/k8s/keycape/registration-adapter.md @@ -13,7 +13,12 @@ catalog/acceptance/run-sheet digests and target. The descriptor pins this adapte and its complete local helper import closure together with the exact reviewed `check-config` executable digest. Changing that digest changes the descriptor source pin and descriptor bytes, requiring independently reviewed admission. -The fixed executable is rehashed before each attempt. The caller constructs the +The fixed executable is rehashed before each attempt. The helper resolves the +selected file to an absolute executable path and supplies an empty checker +environment, so ambient PATH and KeyCape URL overrides cannot select another +executable or alter validation of supplied bytes. Path resolution does not +establish immutable storage or eliminate replacement races; executable custody +and native runtime admission remain separate requirements. The caller constructs the proposed invocation from exact source inputs, installs the fixed adapter once, and calls normal `Executor.admit`; independently derived admission must match. No callable replacement after admission is required or supported by this recipe. diff --git a/sso-mfa/k8s/keycape/test_keycape_checker_environment.py b/sso-mfa/k8s/keycape/test_keycape_checker_environment.py new file mode 100644 index 0000000..6e73810 --- /dev/null +++ b/sso-mfa/k8s/keycape/test_keycape_checker_environment.py @@ -0,0 +1,66 @@ +"""Opt-in real-checker boundary regressions; no production inputs or checker stub. + +Set KEYCAPE_TEST_CHECKER_BINARY and KEYCAPE_TEST_CHECKER_SHA256 to an independently +built artifact. Ordinary helper-only runs skip this class when none is supplied. +""" +from contextlib import chdir +import hashlib +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from test_keycape_client import kc + +VALID = '''issuer: https://issuer.example.invalid +port: 8080 +privateKeyPem: /nonexistent/synthetic-key.pem +clients: + - clientId: synthetic-client + displayName: Synthetic client + redirectUris: [https://client.example.invalid/cb] + allowedScopes: [openid] + grantTypes: [authorization_code] + clientType: public +''' + + +class RealCheckerEnvironmentTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + source = os.environ.get('KEYCAPE_TEST_CHECKER_BINARY') + if not source: + raise unittest.SkipTest('exact real checker artifact not supplied') + data = Path(source).read_bytes() + expected = os.environ.get('KEYCAPE_TEST_CHECKER_SHA256') + if expected != 'sha256:' + hashlib.sha256(data).hexdigest(): + raise ValueError('real checker artifact digest differs') + cls.directory = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.directory.cleanup) + cls.binary = Path(cls.directory.name) / 'keycape' + cls.binary.write_bytes(data) + cls.binary.chmod(0o700) + + def check(self, raw, binary=None): + return kc.check_config(str(binary or self.binary), raw, [], False) + + def test_ambient_overrides_cannot_reject_supplied_valid_config(self): + with patch.dict(os.environ, {'KEYCAPE_ACCOUNT_PORTAL_URL': 'http://polluted.example.invalid', + 'KEYCAPE_BROWSER_LOGOUT_URL': 'http://polluted.example.invalid/logout'}): + self.assertTrue(self.check(VALID)['valid']) + + def test_ambient_overrides_cannot_admit_supplied_invalid_config(self): + invalid = VALID + 'accountPortalURL: http://invalid.example.invalid\nbrowserLogoutURL: http://invalid.example.invalid/logout\n' + with patch.dict(os.environ, {'KEYCAPE_ACCOUNT_PORTAL_URL': 'https://polluted.example.invalid', + 'KEYCAPE_BROWSER_LOGOUT_URL': 'https://polluted.example.invalid/logout'}): + self.assertFalse(self.check(invalid)['valid']) + + def test_relative_selected_artifact_is_not_replaced_by_path_decoy(self): + decoy = Path(self.directory.name) / 'decoy' + decoy.mkdir() + trap = decoy / 'keycape' + trap.write_text('#!/bin/sh\nexit 27\n') + trap.chmod(0o700) + with chdir(self.directory.name), patch.dict(os.environ, {'PATH': str(decoy)}): + self.assertTrue(self.check(VALID, binary=Path('keycape'))['valid']) diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md index efc9985..7ca0237 100644 --- a/workplans/NK-WP-0048-security-case-types.md +++ b/workplans/NK-WP-0048-security-case-types.md @@ -323,7 +323,7 @@ the rationale. The type is `acknowledged`; it becomes `implemented` with T06. ```task id: NK-WP-0048-T06 status: wait -depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, MASON-WP-0010-T03, WARDEN-WP-0044-T03] +depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, NK-WP-0048-T17, NK-WP-0048-T18, MASON-WP-0010-T03, WARDEN-WP-0044-T03] blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status." priority: high state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" @@ -734,10 +734,11 @@ state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e" ``` Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden -independently owns global provenance/admission. Next action: identify the exact -reviewed checker executable artifact and native owner evidence/session callback -contracts, then assemble the complete step descriptor set and a proposed explicit -type version selection. T13's retained descriptor pins a synthetic checker only. +independently owns global provenance/admission. Next action: obtain review of the +exact checker source/artifact proposed by T17 and identify native owner evidence/ +session callback contracts, then assemble the complete step descriptor set and +a proposed explicit type version selection. T13's historical descriptor retains +its synthetic checker; T17 supplies an artifact-bound proposal, not admission. Completion requires separate owner source review of exact machine descriptors, artifact/source/config pins, normal type/version admission and independent evidence-verification boundaries. Warden d97652a refreshes producer validator @@ -746,3 +747,70 @@ machine descriptor into an acknowledged type is a separately reviewed source change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual consumer login/group enforcement. Neither their assent nor native acceptance is supplied by this task. + +## T17 — Build and exercise the exact offline config-checker artifact + +```task +id: NK-WP-0048-T17 +status: done +priority: high +``` + +Completed 2026-10-16, isolated artifact/source fixture proof only. Built the +existing `key-cape/src/cmd/keycape` command from published clean source revision +`3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, source tree +`f0010b9cab17dbc3a787628a29657d98b900320f`, with cached Go 1.23.0, +CGO disabled, Linux/amd64 v1, trimmed paths, disabled VCS stamping and offline +read-only module selection. The full retained closure pins 108 source files, +9876 toolchain files and ten used module source directories. Three isolated +build locations produced identical 13633409-byte artifacts: +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. + +Eight disposable checks use the actual executable and maintained helper/kernel, +with complete synthetic configs and a minimal environment: add/created-only +removal, normal pinned producer admission, rollback, lost-readback uncertainty, +stale revisions, substituted checker refusal, invalid startup config and missing +secret-env refusal. No checker stub or producer-pin override remains. The +artifact-bound descriptor proposal is +`sha256:76b1153d188062ece53fcd5e8d35290fdbfd02a69dd79d4b001fc42781afcdc3`. +Evidence: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-checker-artifact-next/`. +The first two build attempts succeeded but their closure collection failed; +both failures are retained, as is refusal of the older incomplete synthetic +fixture. No production config, credentials or cluster mutation was used. + +This extracts completed build/fixture scope from T16 without claiming KeyCape +owner endorsement, native checker selection/storage custody, acknowledged type +integration, native evidence/session callbacks or attended browser acceptance. +T16 retains exact owner review and composition/admission scope. The attended +registration procedure remains unexercised. + +## T18 — Make checker input and executable selection deterministic + +```task +id: NK-WP-0048-T18 +status: done +priority: high +``` + +Completed 2026-10-16, isolated source correction and real-artifact fixture proof +only; independent integration/publication remain separately recorded. KeyCape +source review identified non-secret `KEYCAPE_ACCOUNT_PORTAL_URL` and +`KEYCAPE_BROWSER_LOGOUT_URL` overrides in config.Load. The original helper +inherited ambient environment, allowing valid supplied configuration to be +rejected or invalid supplied URL configuration to be admitted. A bare relative +checker filename could also be replaced by PATH lookup after the adapter had +hashed the selected local file. All three counterexamples are retained against +the unchanged predecessor. + +The existing helper now resolves the selected checker file to an absolute path +and passes an explicit empty subprocess environment. Three opt-in real-checker +regressions verify both URL-verdict directions and relative-file selection under +a non-checker PATH decoy. All 39 affected checks pass with the exact supplied +artifact, as do 11 real-artifact helper/kernel scenarios. Source evidence: +Custodian `docs/changes/netkingdom-target-2026-10-16/browser-checker-environment-r2/`. +Historical descriptors remain retained; the helper correction changes successor +source-composition pins and requires their separate review/admission. An absolute +path is not immutable custody and does not eliminate file replacement races; +T16 retains native executable storage/custody and callback/type admission. +No production configuration, credentials or cluster mutation was used.