Bind mint broker validation to exact reviewed sources and track successor obligations
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
parent
104ed90f42
commit
d35c0f9d4b
5 changed files with 337 additions and 8 deletions
|
|
@ -137,9 +137,9 @@ grants:
|
||||||
requires_human_above: 5m
|
requires_human_above: 5m
|
||||||
actors:
|
actors:
|
||||||
allowed_types:
|
allowed_types:
|
||||||
- approved-agent
|
|
||||||
- human-operator
|
- human-operator
|
||||||
required_subject_binding: keycape-or-kubernetes-service-account
|
required_subject_binding: auth/netkingdom/role/credential-broker-keycape-client-secret-generate
|
||||||
|
# CCR-2026-0043: human net-kingdom-admins membership and /assurance/level=aal2.
|
||||||
authorization:
|
authorization:
|
||||||
flex_auth_required: false
|
flex_auth_required: false
|
||||||
flex_auth_mode: optional-preflight
|
flex_auth_mode: optional-preflight
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Steps (every one fails closed):
|
||||||
the broker issuer policy for this grant.
|
the broker issuer policy for this grant.
|
||||||
2. Re-derive the case reference from ``--case-file`` (sha256 over its exact
|
2. Re-derive the case reference from ``--case-file`` (sha256 over its exact
|
||||||
bytes) and require: it equals ``--case-ref``; type
|
bytes) and require: it equals ``--case-ref``; type
|
||||||
``oidc-browser-relying-party@1.1``; status ``materialized``; client_type
|
an exactly source-pinned acknowledged browser revision; status ``materialized``; client_type
|
||||||
``confidential``; ``parameters.client_id`` equals ``--client-id``, which
|
``confidential``; ``parameters.client_id`` equals ``--client-id``, which
|
||||||
matches the type's pattern and is not a reserved entry.
|
matches the type's pattern and is not a reserved entry.
|
||||||
3. Mint a 5-minute grant token through token role
|
3. Mint a 5-minute grant token through token role
|
||||||
|
|
@ -46,6 +46,7 @@ import re
|
||||||
import secrets
|
import secrets
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -57,7 +58,39 @@ ISSUER_POLICY = 'credential-broker-keycape-client-secret-generate-issuer'
|
||||||
PREFIX = 'workloads/keycape'
|
PREFIX = 'workloads/keycape'
|
||||||
RESERVED = {'secrets-engine-openbao'}
|
RESERVED = {'secrets-engine-openbao'}
|
||||||
FIELD = 'CLIENT_SECRET'
|
FIELD = 'CLIENT_SECRET'
|
||||||
CASE_TYPE = 'oidc-browser-relying-party@1.1'
|
# Owner configuration, never a caller-supplied root or a version search/fallback.
|
||||||
|
# The sibling source checkout must contain these exact reviewed bytes.
|
||||||
|
NETKINGDOM_ROOT = Path(__file__).resolve().parents[2] / 'net-kingdom'
|
||||||
|
ACKNOWLEDGED_TYPES = {
|
||||||
|
'oidc-browser-relying-party@1.1': (
|
||||||
|
'security-case-types/oidc-browser-relying-party/type.yaml',
|
||||||
|
'f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c'),
|
||||||
|
}
|
||||||
|
# Same authenticated producer and parameter projection as ops-warden case_inputs
|
||||||
|
# at b593c879, restricted to this broker's currently admitted 1.1. Source-only
|
||||||
|
# acknowledgment (including 1.2) never expands broker admission. A future 1.3
|
||||||
|
# entry requires real acknowledgment and a separately reviewed admission binding.
|
||||||
|
# Neither source status nor caller-supplied YAML expands this set.
|
||||||
|
SOURCE_PINS = {
|
||||||
|
'tools/security-case-types/security_case_validator.py':
|
||||||
|
'1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b',
|
||||||
|
'tools/security-scenario-composer/security_scenario_composer.py':
|
||||||
|
'9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f',
|
||||||
|
'tools/playbook-capability-contract/playbook_contract_validator.py':
|
||||||
|
'2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d',
|
||||||
|
}
|
||||||
|
PARAMETER_PINS = {
|
||||||
|
'1.1': '9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c',
|
||||||
|
}
|
||||||
|
_VALIDATE = """
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types'))
|
||||||
|
import security_case_validator as v
|
||||||
|
case, typ = json.load(sys.stdin)
|
||||||
|
issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False)
|
||||||
|
print('valid' if not v.has_errors(issues) else 'invalid')
|
||||||
|
"""
|
||||||
# oidc-browser-relying-party@1.1 parameter client_id.
|
# oidc-browser-relying-party@1.1 parameter client_id.
|
||||||
CLIENT_ID_RE = re.compile(r'^[a-z0-9][a-z0-9-]{1,62}$')
|
CLIENT_ID_RE = re.compile(r'^[a-z0-9][a-z0-9-]{1,62}$')
|
||||||
CASE_REF_RE = re.compile(r'^[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64}$')
|
CASE_REF_RE = re.compile(r'^[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64}$')
|
||||||
|
|
@ -95,6 +128,53 @@ def read_json(result, code):
|
||||||
raise Exit(code, 'unparseable_bao_output') from None
|
raise Exit(code, 'unparseable_bao_output') from None
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated_source(relative, expected):
|
||||||
|
path = NETKINGDOM_ROOT / relative
|
||||||
|
# Refuse symlinks at every selected component, including the configured root.
|
||||||
|
if any(part.is_symlink() for part in (path, *path.parents)):
|
||||||
|
raise ValueError
|
||||||
|
raw = path.read_bytes()
|
||||||
|
if hashlib.sha256(raw).hexdigest() != expected:
|
||||||
|
raise ValueError
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def check_acknowledged_case(doc):
|
||||||
|
# Authenticates the selected TYPE/producer bytes and validates case schema.
|
||||||
|
# The caller's self-hashed case reference is not authenticated case admission
|
||||||
|
# or owner acceptance. Those require independent Warden/native evidence;
|
||||||
|
# this source-only hardening does not complete the real admission binding.
|
||||||
|
binding = ACKNOWLEDGED_TYPES.get(doc['spec'].get('type'))
|
||||||
|
if binding is None:
|
||||||
|
raise Exit(2, 'case_type_unacknowledged')
|
||||||
|
try:
|
||||||
|
typ = yaml.safe_load(authenticated_source(*binding))
|
||||||
|
spec = typ['spec']
|
||||||
|
version = str(spec['version'])
|
||||||
|
fingerprint = hashlib.sha256(json.dumps(
|
||||||
|
spec['parameters'], sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||||
|
if (spec['status'] not in ('acknowledged', 'implemented')
|
||||||
|
or f"{spec['id']}@{version}" != doc['spec']['type']
|
||||||
|
or PARAMETER_PINS.get(version) != fingerprint):
|
||||||
|
raise ValueError
|
||||||
|
with tempfile.TemporaryDirectory(prefix='platform-case-validator-') as directory:
|
||||||
|
for relative, expected in SOURCE_PINS.items():
|
||||||
|
raw = authenticated_source(relative, expected)
|
||||||
|
dest = Path(directory) / relative
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest.write_bytes(raw)
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, '-I', '-c', _VALIDATE, directory],
|
||||||
|
input=json.dumps([doc, typ]), text=True,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
||||||
|
env={}, timeout=10, check=True)
|
||||||
|
if result.stdout != 'valid\n':
|
||||||
|
raise ValueError
|
||||||
|
except (OSError, ValueError, TypeError, KeyError, yaml.YAMLError,
|
||||||
|
subprocess.SubprocessError):
|
||||||
|
raise Exit(2, 'case_source_validation_failed') from None
|
||||||
|
|
||||||
|
|
||||||
def check_case(args, receipt):
|
def check_case(args, receipt):
|
||||||
if not CLIENT_ID_RE.match(args.client_id) or args.client_id in RESERVED:
|
if not CLIENT_ID_RE.match(args.client_id) or args.client_id in RESERVED:
|
||||||
raise Exit(2, 'client_id_invalid_or_reserved')
|
raise Exit(2, 'client_id_invalid_or_reserved')
|
||||||
|
|
@ -112,14 +192,15 @@ def check_case(args, receipt):
|
||||||
derived = f"{spec.get('id')}@sha256:{hashlib.sha256(raw).hexdigest()}"
|
derived = f"{spec.get('id')}@sha256:{hashlib.sha256(raw).hexdigest()}"
|
||||||
if derived != args.case_ref:
|
if derived != args.case_ref:
|
||||||
raise Exit(2, 'case_ref_mismatch')
|
raise Exit(2, 'case_ref_mismatch')
|
||||||
if spec.get('type') != CASE_TYPE:
|
if not isinstance(spec.get('type'), str):
|
||||||
raise Exit(2, 'case_type_mismatch')
|
raise Exit(2, 'case_type_unacknowledged')
|
||||||
if spec.get('status') != 'materialized':
|
if spec.get('status') != 'materialized':
|
||||||
raise Exit(2, 'case_not_materialized')
|
raise Exit(2, 'case_not_materialized')
|
||||||
if params.get('client_type') != 'confidential':
|
if params.get('client_type') != 'confidential':
|
||||||
raise Exit(2, 'case_client_not_confidential')
|
raise Exit(2, 'case_client_not_confidential')
|
||||||
if params.get('client_id') != args.client_id:
|
if params.get('client_id') != args.client_id:
|
||||||
raise Exit(2, 'case_client_id_mismatch')
|
raise Exit(2, 'case_client_id_mismatch')
|
||||||
|
check_acknowledged_case(doc)
|
||||||
receipt['case_verified'] = True
|
receipt['case_verified'] = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,20 @@ class CredentialGrantBindTests(unittest.TestCase):
|
||||||
grants.validate_grant(grant, index, catalog, errors)
|
grants.validate_grant(grant, index, catalog, errors)
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
|
def test_keycape_generator_actor_matches_applied_issuer_binding(self) -> None:
|
||||||
|
catalog = grants.yaml.safe_load(
|
||||||
|
(REPO_DIR / 'credential-grants/catalog.yaml').read_text())
|
||||||
|
grant = next(g for g in catalog['grants'] if g['id'] == 'keycape/client-secret-generate')
|
||||||
|
ccr = grants.yaml.safe_load((REPO_DIR / 'credential-change-requests' /
|
||||||
|
'CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml').read_text())
|
||||||
|
role = grants.yaml.safe_load((REPO_DIR / ccr['openbao']['auth']['role_file']).read_text())
|
||||||
|
auth = ccr['openbao']['auth']
|
||||||
|
self.assertEqual(grant['actors']['allowed_types'], ['human-operator'])
|
||||||
|
self.assertEqual(grant['actors']['required_subject_binding'],
|
||||||
|
f"auth/{auth['mount']}/role/{auth['role']}")
|
||||||
|
self.assertEqual(role['bound_claims'],
|
||||||
|
{'groups': ['net-kingdom-admins'], '/assurance/level': 'aal2'})
|
||||||
|
|
||||||
def test_postgres_grants_bind_rapp(self) -> None:
|
def test_postgres_grants_bind_rapp(self) -> None:
|
||||||
catalog = grants.yaml.safe_load(
|
catalog = grants.yaml.safe_load(
|
||||||
(REPO_DIR / "credential-grants/catalog.yaml").read_text()
|
(REPO_DIR / "credential-grants/catalog.yaml").read_text()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ token each call used: KV calls must use the minted grant token, never the
|
||||||
caller's session.
|
caller's session.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import shutil
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import yaml
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -75,7 +80,9 @@ class Generate(unittest.TestCase):
|
||||||
def run_cli(self, *extra, client_id='forgejo', ref=None, env_extra=None):
|
def run_cli(self, *extra, client_id='forgejo', ref=None, env_extra=None):
|
||||||
(self.tmp / 'state.json').write_text(json.dumps(self.state))
|
(self.tmp / 'state.json').write_text(json.dumps(self.state))
|
||||||
receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json'
|
receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json'
|
||||||
env = {k: v for k, v in os.environ.items() if k not in ('BAO_TOKEN', 'VAULT_TOKEN', 'WARDEN_AGENT_ID')}
|
empty_home = self.tmp / 'empty-home'
|
||||||
|
empty_home.mkdir(exist_ok=True)
|
||||||
|
env = {'PATH': os.defpath, 'HOME': str(empty_home)}
|
||||||
env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', FAKE_BAO_STATE=str(self.tmp / 'state.json'),
|
env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', FAKE_BAO_STATE=str(self.tmp / 'state.json'),
|
||||||
**(env_extra or {}))
|
**(env_extra or {}))
|
||||||
proc = subprocess.run([sys.executable, str(CLI), 'generate', '--grant', 'keycape/client-secret-generate',
|
proc = subprocess.run([sys.executable, str(CLI), 'generate', '--grant', 'keycape/client-secret-generate',
|
||||||
|
|
@ -149,15 +156,124 @@ class Generate(unittest.TestCase):
|
||||||
def test_only_exact_acknowledged_type_version_is_admitted(self):
|
def test_only_exact_acknowledged_type_version_is_admitted(self):
|
||||||
for type_ref in ('oidc-browser-relying-party@1.0',
|
for type_ref in ('oidc-browser-relying-party@1.0',
|
||||||
'oidc-browser-relying-party@1.2',
|
'oidc-browser-relying-party@1.2',
|
||||||
|
'oidc-browser-relying-party@1.3',
|
||||||
'oidc-browser-relying-party@99.0',
|
'oidc-browser-relying-party@99.0',
|
||||||
'confidential-service-client@1.1'):
|
'confidential-service-client@1.1'):
|
||||||
with self.subTest(type_ref=type_ref):
|
with self.subTest(type_ref=type_ref):
|
||||||
self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref))
|
self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref))
|
||||||
self.state['calls'] = []
|
self.state['calls'] = []
|
||||||
code, receipt = self.run_cli()
|
code, receipt = self.run_cli()
|
||||||
self.assertEqual((code, receipt['outcome']), (2, 'case_type_mismatch'))
|
self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged'))
|
||||||
self.assertEqual(self.state['calls'], [])
|
self.assertEqual(self.state['calls'], [])
|
||||||
|
|
||||||
|
def test_source_acknowledgment_alone_does_not_admit_12(self):
|
||||||
|
self.write_case(CASE.replace('oidc-browser-relying-party@1.1',
|
||||||
|
'oidc-browser-relying-party@1.2')
|
||||||
|
+ ' consumer_namespace: forgejo\n')
|
||||||
|
code, receipt = self.run_cli()
|
||||||
|
self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged'))
|
||||||
|
self.assertEqual(self.state['calls'], [])
|
||||||
|
|
||||||
|
def test_caller_environment_cannot_select_type_or_source_root(self):
|
||||||
|
code, receipt = self.run_cli(env_extra={
|
||||||
|
'NETKINGDOM_ROOT': '/nonexistent',
|
||||||
|
'WARDEN_NETKINGDOM_ROOT': '/nonexistent',
|
||||||
|
'PYTHONPATH': '/nonexistent',
|
||||||
|
})
|
||||||
|
self.assertEqual((code, receipt['outcome']), (0, 'generated'))
|
||||||
|
|
||||||
|
def test_acknowledged_11_still_requires_its_exact_parameters(self):
|
||||||
|
self.write_case(CASE + ' unexpected_target: forgejo\n')
|
||||||
|
code, receipt = self.run_cli()
|
||||||
|
self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed'))
|
||||||
|
self.assertEqual(self.state['calls'], [])
|
||||||
|
|
||||||
|
def test_future_revision_requires_explicit_owner_binding_and_acknowledgment(self):
|
||||||
|
# Synthetic owner configuration exercises the extension mechanism only.
|
||||||
|
# This is not the actual proposed 1.3 type, acknowledgment or parameter pin.
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
'future_fixture_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
|
||||||
|
broker = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(broker)
|
||||||
|
source = self.tmp / 'synthetic-owner-root'
|
||||||
|
source.mkdir()
|
||||||
|
relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1']
|
||||||
|
for rel in (relative, *broker.SOURCE_PINS):
|
||||||
|
dest = source / rel
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest)
|
||||||
|
typ = source / relative
|
||||||
|
original = typ.read_bytes()
|
||||||
|
doc = yaml.safe_load(CASE)
|
||||||
|
doc['spec']['type'] = 'oidc-browser-relying-party@1.3'
|
||||||
|
typ.write_bytes(original.replace(b'version: "1.1"', b'version: "1.3"'))
|
||||||
|
# YAML numeric spelling is owner-authored, never an inferred revision.
|
||||||
|
type_doc = yaml.safe_load(original)
|
||||||
|
type_doc['spec']['version'] = '1.3'
|
||||||
|
typ.write_text(yaml.safe_dump(type_doc))
|
||||||
|
binding = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}
|
||||||
|
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
|
||||||
|
with self.assertRaises(broker.Exit):
|
||||||
|
broker.check_acknowledged_case(doc) # no explicit binding
|
||||||
|
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, binding):
|
||||||
|
with self.assertRaises(broker.Exit):
|
||||||
|
broker.check_acknowledged_case(doc) # no parameter admission
|
||||||
|
with mock.patch.dict(broker.PARAMETER_PINS, {'1.3': broker.PARAMETER_PINS['1.1']}):
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
type_doc['spec']['status'] = 'proposed'
|
||||||
|
typ.write_text(yaml.safe_dump(type_doc))
|
||||||
|
proposed = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}
|
||||||
|
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, proposed):
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
|
||||||
|
|
||||||
|
def test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed(self):
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
'candidate_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
|
||||||
|
broker = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(broker)
|
||||||
|
source = self.tmp / 'net-kingdom'
|
||||||
|
source.mkdir()
|
||||||
|
relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1']
|
||||||
|
for rel in (relative, *broker.SOURCE_PINS):
|
||||||
|
dest = source / rel
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest)
|
||||||
|
doc = yaml.safe_load(CASE)
|
||||||
|
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
producer = source / next(iter(broker.SOURCE_PINS))
|
||||||
|
raw = producer.read_bytes()
|
||||||
|
producer.write_bytes(raw + b'\n# drift\n')
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
|
||||||
|
producer.write_bytes(raw)
|
||||||
|
typ = source / relative
|
||||||
|
raw = typ.read_bytes()
|
||||||
|
typ.write_bytes(raw.replace(b'status: acknowledged', b'status: proposed'))
|
||||||
|
# Even a hypothetical owner pin update cannot admit proposed status.
|
||||||
|
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, {
|
||||||
|
doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}):
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
|
||||||
|
typ.write_bytes(raw + b'\n# source drift\n')
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
|
||||||
|
typ.unlink()
|
||||||
|
typ.symlink_to(broker.__file__)
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
|
||||||
|
for ref in ('oidc-browser-relying-party@1.3', 'oidc-browser-relying-party@99.0'):
|
||||||
|
doc['spec']['type'] = ref
|
||||||
|
with self.assertRaises(broker.Exit) as caught:
|
||||||
|
broker.check_acknowledged_case(doc)
|
||||||
|
self.assertEqual(caught.exception.reason, 'case_type_unacknowledged')
|
||||||
|
|
||||||
def test_exact_accepted_case_still_requires_materialization(self):
|
def test_exact_accepted_case_still_requires_materialization(self):
|
||||||
self.write_case(CASE.replace('status: materialized', 'status: accepted'))
|
self.write_case(CASE.replace('status: materialized', 'status: accepted'))
|
||||||
code, receipt = self.run_cli()
|
code, receipt = self.run_cli()
|
||||||
|
|
|
||||||
118
workplans/RPF-WP-0060-browser-case-platform-bindings.md
Normal file
118
workplans/RPF-WP-0060-browser-case-platform-bindings.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
---
|
||||||
|
id: RPF-WP-0060
|
||||||
|
type: workplan
|
||||||
|
title: "Implement Platform bindings for the browser SecurityCase successor"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: active
|
||||||
|
owner: codex
|
||||||
|
topic_slug: netkingdom
|
||||||
|
created: "2026-10-17"
|
||||||
|
updated: "2026-10-17"
|
||||||
|
flavor: implementation
|
||||||
|
origin: cross-repo
|
||||||
|
origin_ref: NK-WP-0048-T16
|
||||||
|
state_hub_workstream_id: "43110f82-ef9f-5a46-a305-f0e287b7b226"
|
||||||
|
---
|
||||||
|
|
||||||
|
Demand: Platform's source review of the proposed browser SecurityCase 1.3
|
||||||
|
requires four concrete owner obligations before owner-descriptor acceptance.
|
||||||
|
The review labels `RPF-1.3-T1` through `T4` were not live work records; this
|
||||||
|
workplan makes them ordinary owner tasks without changing RPF-WP-0054's
|
||||||
|
completed first-generation lane or implementing its residual rotation work.
|
||||||
|
|
||||||
|
Review: `the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-1.3-owner-review/owner-review-railiance-platform.json`.
|
||||||
|
The 1.3 type remains proposed. This workplan grants no native access, case
|
||||||
|
acceptance, owner acknowledgment, catalog activation or policy change.
|
||||||
|
|
||||||
|
## T01 — Bind the broker to reviewed acknowledged type sources
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RPF-WP-0060-T01
|
||||||
|
status: progress
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "afec2053-1995-506d-a112-cda1de552c1c"
|
||||||
|
```
|
||||||
|
|
||||||
|
Implements review obligation RPF-1.3-T1. The broker's explicit source map admits
|
||||||
|
only its existing 1.1 revision. Exact type and producer bytes, parameter
|
||||||
|
fingerprint, acknowledgment state and case schema are checked before any Bao
|
||||||
|
call. Source-only 1.2 and proposed 1.3 remain refused. The fixed installation
|
||||||
|
layout is `railiance-platform` and `net-kingdom` as real sibling directories;
|
||||||
|
missing, symlinked or drifted selected sources refuse rather than fall back.
|
||||||
|
|
||||||
|
2026-10-17: source hardening and catalog regressions independently reviewed in
|
||||||
|
`the-custodian/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/`.
|
||||||
|
The exact case reference binds caller-supplied bytes; schema validation does
|
||||||
|
not authenticate case acceptance. Existing Warden admission/native evidence
|
||||||
|
must establish that independently. This task remains open for the real binding
|
||||||
|
and explicit 1.3 source/parameter pins after genuine acknowledgment and review.
|
||||||
|
No speculative successor pin or acknowledgment is added.
|
||||||
|
|
||||||
|
Done when the actual admitted-case binding is verified, the genuinely
|
||||||
|
acknowledged successor is explicitly selected, and refusal tests cover unknown,
|
||||||
|
unacknowledged, wrong-case and drifted-source inputs before native effects.
|
||||||
|
|
||||||
|
## T02 — Correct the documented mint caller and issuer binding
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RPF-WP-0060-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "1a764c10-3a41-5eff-af96-c9e33a6e2c8b"
|
||||||
|
```
|
||||||
|
|
||||||
|
Implements review obligation RPF-1.3-T2. `credential-grants/catalog.yaml` now
|
||||||
|
names human-operator only and the exact existing issuer role
|
||||||
|
`auth/netkingdom/role/credential-broker-keycape-client-secret-generate`.
|
||||||
|
Regression checks bind this statement to CCR-2026-0043's role source,
|
||||||
|
net-kingdom-admins membership and aal2 claim. No OpenBao role, policy, grant,
|
||||||
|
credential or live configuration changes.
|
||||||
|
|
||||||
|
## T03 — Implement mint, guarded destruction and authenticated evidence
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RPF-WP-0060-T03
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "b5d73ec6-c593-50a9-8367-f899edec6f5f"
|
||||||
|
```
|
||||||
|
|
||||||
|
Implements review obligation RPF-1.3-T3. Bind the actual generator to the closed
|
||||||
|
`railiance-platform.custody-mint.v1` projection and authenticated publication.
|
||||||
|
Capture grant accessor and revocation evidence and revoke the issuer session.
|
||||||
|
Destruction uses attended platform-admin authority and compares the original
|
||||||
|
CAS-0 version-1 creation time, current version and absence of other versions
|
||||||
|
before metadata deletion. No standing family-wide delete grant is introduced.
|
||||||
|
Unavailable authority, uncertain effects or failed revocation remain UNCERTAIN
|
||||||
|
for owner reconciliation. Verify with the frozen adapter, then obtain the
|
||||||
|
required native authority and actual execution receipts.
|
||||||
|
|
||||||
|
## T04 — Implement and admit the delivery lane and capability
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RPF-WP-0060-T04
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "84f8b0a4-8450-5623-8608-dc277978383b"
|
||||||
|
```
|
||||||
|
|
||||||
|
Implements review obligation RPF-1.3-T4. Reuse the fixed proposed issuer Store
|
||||||
|
contract; validate the preexisting Store and only create the case-derived
|
||||||
|
ExternalSecret in `sso`. Refuse conflicts; report NO_CHANGE only for matching
|
||||||
|
preexisting configuration; establish current Ready status before success.
|
||||||
|
Return the closed metadata-only `netkingdom.deliver-secret.v1` receipt and use
|
||||||
|
UID and resourceVersion preconditions to remove only authenticated case-created
|
||||||
|
resources. Never read Secret values or create the Store through this lane.
|
||||||
|
|
||||||
|
Bind real human net-kingdom-admins/aal2 admission, per-attempt session and
|
||||||
|
revocation, durable pre-intent/recovery journal and authenticated owner evidence.
|
||||||
|
The separate command-mechanics candidate is preparation, not this capability;
|
||||||
|
its production factory refuses. Review freshness/defaulting/transport failure
|
||||||
|
cases before integrating it. Obtain ops-warden's catalog routing acceptance for
|
||||||
|
`platform-secret-delivery` through an authorized handoff before recording owner
|
||||||
|
acceptance. No routing handoff or native action is implied by this work record.
|
||||||
|
|
||||||
|
The parent stays active until these obligations and their required owner
|
||||||
|
acceptance are proved. Source tests do not complete native acceptance or
|
||||||
|
KEY-WP-0039's negative/expiry/cleanup exercise.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue