diff --git a/docs/keycape-isolated-recovery-selector.md b/docs/keycape-isolated-recovery-selector.md new file mode 100644 index 0000000..790daa2 --- /dev/null +++ b/docs/keycape-isolated-recovery-selector.md @@ -0,0 +1,38 @@ +# KeyCape original service recovery selector + +Source-owner disposition, 2026-10-17, limited to secrets-engine's current consumer +contract. Current src/secrets_engine/service_auth.py fixes client/audience +secrets-engine-openbao, subject service:secrets-engine, tenant tenant:platform, +role secrets-engine and exact openbao:login scope. The NetKingdom producer binds +KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET; original KeyCape native evidence +uses openbao:admin as the denied-scope case. This selector matches the current +consumer source. This disposition is not native assent or a credential grant. + +SECRETS-WP-0012 finished on historical attended custody/probe evidence and stays +finished. SECRETS-WP-0008-T06 retains functional steady-state adoption and scoped +backend entitlement. SECRETS-WP-0013 retains task-scoped backend execution. The +new SECRETS-WP-0015 covers only this original archive/isolated consumer preparation +and later accepted continuity proof; those existing gates are not duplicated. + +The current consumer profile forbids a client-secret file and requires its +attended in-memory OpenBao source. That source depends on ordinary KeyCape OIDC +and OpenBao availability. The isolated issuer's native checker can consume its +original archive-delivered issuer binding without a provider login, but that does +not prove the secrets-engine consumer path works during an OpenBao/issuer outage. +Do not bypass the no-file/source contract, retrieve a live credential, redirect a +production consumer or invoke probe-openbao to fill this recovery gap. An exact +owner-reviewed outage-compatible proof/custody plan remains required. + +Offline public preflight, after integration of reviewed source: + +```sh +python3 tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json --source-sha256 sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0 +``` + +Use the exact hash in the owner packet, not a digest inferred at invocation from +an unreviewed file. The tool parses source constants with AST and never imports +the credential/backend module or calls the network. It requires all eight public +selector fields, exact source hash and unambiguous bounded JSON. Successful output +means source correspondence only. Original archived presence/secret binding, +actual TLS trust, native claim negatives and selected consumer continuity remain +separately evidenced. Missing/changed bindings refuse before any custody request. diff --git a/docs/keycape-recovery-selector.json b/docs/keycape-recovery-selector.json new file mode 100644 index 0000000..5e09253 --- /dev/null +++ b/docs/keycape-recovery-selector.json @@ -0,0 +1,15 @@ +{ + "schema": "secrets-engine.public-recovery-selector.v1", + "status": "source_correspondence_only_not_native_assent", + "issuer": "https://kc.coulomb.social", + "consumer": { + "client_id": "secrets-engine-openbao", + "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", + "audience": "secrets-engine-openbao", + "scope": "openbao:login", + "subject": "service:secrets-engine", + "tenant": "tenant:platform", + "roles": "secrets-engine", + "deny_scope": "openbao:admin" + } +} diff --git a/tests/test_keycape_recovery_selector.py b/tests/test_keycape_recovery_selector.py new file mode 100644 index 0000000..46667a6 --- /dev/null +++ b/tests/test_keycape_recovery_selector.py @@ -0,0 +1,31 @@ +import hashlib +import importlib.util +import json +from pathlib import Path +import pytest + +spec=importlib.util.spec_from_file_location('check',Path(__file__).parents[1]/'tools/check-keycape-recovery-selector.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +SOURCE=b'CLIENT_ID="secrets-engine-openbao"\nSUBJECT="service:secrets-engine"\nTENANT="tenant:platform"\nROLE="secrets-engine"\nSCOPE="openbao:login"\nraise RuntimeError("source is parsed, never executed")\n' +SELECTOR={'client_id':'secrets-engine-openbao','secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET','audience':'secrets-engine-openbao','scope':'openbao:login','subject':'service:secrets-engine','tenant':'tenant:platform','roles':'secrets-engine','deny_scope':'openbao:admin'} +DIGEST='sha256:'+hashlib.sha256(SOURCE).hexdigest() +def test_source_parsed_not_executed(): + result=m.verify(SELECTOR,SOURCE,DIGEST) + assert result['network_contacted'] is False +@pytest.mark.parametrize('field',list(SELECTOR)) +def test_each_selector_binding_is_exact(field): + bad={**SELECTOR,field:'different'} + with pytest.raises(ValueError):m.verify(bad,SOURCE,DIGEST) +def test_source_drift_and_untyped_digest_refused(): + for source,digest in ((SOURCE+b'\n',DIGEST),(SOURCE,DIGEST.removeprefix('sha256:'))): + with pytest.raises(ValueError):m.verify(SELECTOR,source,digest) +def test_duplicate_constants_refused(): + source=SOURCE+b'CLIENT_ID="other"\n' + with pytest.raises(ValueError):m.verify(SELECTOR,source,'sha256:'+hashlib.sha256(source).hexdigest()) +def test_duplicate_public_metadata_keys_refused(): + with pytest.raises(ValueError):json.loads('{"consumer":{},"consumer":{}}',object_pairs_hook=m.unique) +def test_oversize_public_input_refused(tmp_path): + p=tmp_path/'selector.json';p.write_bytes(b'x'*(m.LIMIT+1)) + with pytest.raises(ValueError):m.read(p) +def test_symlink_public_input_refused(tmp_path): + p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p) + with pytest.raises(ValueError):m.read(link) diff --git a/tools/check-keycape-recovery-selector.py b/tools/check-keycape-recovery-selector.py new file mode 100644 index 0000000..88d5754 --- /dev/null +++ b/tools/check-keycape-recovery-selector.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Offline public selector preflight; no imports of credential/backend clients. + +Checks current consumer source constants and the producer's already-deployed +public env/denial selector. This is source metadata, not runtime/owner assent. +""" +import argparse +import ast +import hashlib +import json +import os +from pathlib import Path +import re +import stat + +ROOT=Path(__file__).resolve().parents[1] +LIMIT=128*1024 + +def read(path): + path=Path(os.path.abspath(path)) + if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError() + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + with os.fdopen(fd,'rb') as source: + info=os.fstat(source.fileno()) + if not stat.S_ISREG(info.st_mode) or not 0LIMIT:raise ValueError() + return raw + +def unique(pairs): + result={} + for key,value in pairs: + if key in result:raise ValueError() + result[key]=value + return result + +def verify(selector,source,source_sha256): + if not re.fullmatch('sha256:[a-f0-9]{64}',source_sha256) or 'sha256:'+hashlib.sha256(source).hexdigest()!=source_sha256:raise ValueError() + needed={'CLIENT_ID','SUBJECT','TENANT','ROLE','SCOPE'};constants={} + for node in ast.parse(source).body: + if isinstance(node,ast.Assign): + for target in node.targets: + if isinstance(target,ast.Name) and target.id in needed: + if target.id in constants or not isinstance(node.value,ast.Constant) or not isinstance(node.value.value,str):raise ValueError() + constants[target.id]=node.value.value + if set(constants)!=needed:raise ValueError() + expected={'client_id':constants['CLIENT_ID'],'secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET', + 'audience':constants['CLIENT_ID'],'scope':constants['SCOPE'],'subject':constants['SUBJECT'], + 'tenant':constants['TENANT'],'roles':constants['ROLE'],'deny_scope':'openbao:admin'} + if selector!=expected:raise ValueError() + return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent', + 'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False} + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True) + args=p.parse_args() + try: + proposal=json.loads(read(args.selector),object_pairs_hook=unique) + if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError() + result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256) + except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1 + print(json.dumps(result,indent=2));return 0 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md b/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md new file mode 100644 index 0000000..6b32638 --- /dev/null +++ b/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md @@ -0,0 +1,100 @@ +--- +id: SECRETS-WP-0015 +type: workplan +title: "Prepare original KeyCape service selector and isolated consumer continuity" +domain: infotech +repo: secrets-engine +status: ready +owner: codex +topic_slug: custodian +created: "2026-10-17" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013] +state_hub_workstream_id: "68a0642e-4908-5260-89c9-3adce60e6a4d" +--- + +Needed source-owner work for NK-WP-0049-T36/T18. Reviewed against current +Lifecycle INTENT, explicit consumer constants/custody source and dated historical +proof. Ready means work can proceed, not native assent. Finished0012 remains +finished; functional adoption/backend entitlement stay0008T06/0013. No new grant, +secret fetch, provider login, production redirect or external message occurred. + +## Establish exact current intended source selector + +```task +id: SECRETS-WP-0015-T01 +status: done +priority: high +assignee: codex +state_hub_task_id: "a3e0e243-edac-5ade-a3d5-f58dac41633a" +``` + +Completed 2026-10-17 as source review. Current consumer constants and config guards +agree with secrets-engine-openbao client/audience, service:secrets-engine, +tenant:platform, role secrets-engine and only openbao:login. Producer env selector +and historical denied openbao:admin case are independently referenced, without +accepting the producer's native role. Prior native issuance7Oct and consumer +probe10Oct predate reviewed archive16Oct; they neither prove exact archived bytes +nor current native assent. Recorded docs/keycape-isolated-recovery-selector.md. +Evidence: Custodian recovery-owner-handoffs-r1 and prior17-source review. + +## Deliver bounded offline public source selector preflight + +```task +id: SECRETS-WP-0015-T02 +status: done +priority: high +assignee: codex +state_hub_task_id: "eab3b758-10ce-5464-b5f7-3a152ee1327a" +``` + +Completed 2026-10-17 as isolated source preparation. Prepared AST-only source-hash +and all-eight-selector-field checker with bounded strict JSON, source drift, +duplicate, oversize and symlink negatives. It imports no backend/credential client +and never exchanges a token. Tests use public/synthetic metadata only. Canonical +integration/publication/projection must be recorded separately; successful output +is source correspondence, not native acceptance or a new secret source. + +## Prepare outage-compatible original consumer proof without custody bypass + +```task +id: SECRETS-WP-0015-T03 +status: todo +priority: high +assignee: codex +state_hub_task_id: "6262a915-7655-50be-b9f7-481f816d7335" +``` + +Next: map the exact original archive-delivered registration and producer-native +checker result to the intended consumer, then prepare a source-reviewed isolated +proof/custody plan. Current consumer source requires attended OpenBao memory read +and refuses files; ordinary OpenBao/KeyCape outage is therefore a real dependency. +Do not add a fallback, persistent consumer copy or unreviewed injection source. +Clarify which continuity claim can be proved offline and which requires an exact +accepted owner custody/provider path. Bind current original selectors, image/ +source/TLS metadata, negative cases, timeout/abort and cleanup; retain protected +backend operation entitlement in existing0008/0013. Done when that exact plan and +remaining owner dependencies are reviewable, without retrieving any value. + +## Verify the separately admitted actual isolated consumer result + +```task +id: SECRETS-WP-0015-T04 +status: wait +priority: high +assignee: codex +needs_human: true +depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33] +blocking_reason: "Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required." +state_hub_task_id: "aeec6eb0-39e2-5f7c-8f5d-778133edfc73" +``` + +Only a separately admitted owner action can establish actual archived registration, +issuer trust, native negative checks and consumer continuity. Source selector and +historical provider proof do not fill this gate. No production consumer redirect, +new grant, live OpenBao read, backend write or retained-factor MFA is authorized. +Return exact bounded evidence to NK49T36/T18; MFA remains NK49T19. Human gate is +never a worker wakeup. Cross-owner dependency is attached to derived case +secrets15-nk49-readiness in the packet; do not strand the wait or invent a driver.