Bind mint broker validation to exact reviewed sources and track successor obligations
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
codex 2026-10-17 22:55:51 +02:00
parent 104ed90f42
commit d35c0f9d4b
5 changed files with 337 additions and 8 deletions

View file

@ -137,9 +137,9 @@ grants:
requires_human_above: 5m
actors:
allowed_types:
- approved-agent
- 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:
flex_auth_required: false
flex_auth_mode: optional-preflight

View file

@ -16,7 +16,7 @@ Steps (every one fails closed):
the broker issuer policy for this grant.
2. Re-derive the case reference from ``--case-file`` (sha256 over its exact
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
matches the type's pattern and is not a reserved entry.
3. Mint a 5-minute grant token through token role
@ -46,6 +46,7 @@ import re
import secrets
import subprocess
import sys
import tempfile
import urllib.request
import yaml
@ -57,7 +58,39 @@ ISSUER_POLICY = 'credential-broker-keycape-client-secret-generate-issuer'
PREFIX = 'workloads/keycape'
RESERVED = {'secrets-engine-openbao'}
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.
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}$')
@ -95,6 +128,53 @@ def read_json(result, code):
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):
if not CLIENT_ID_RE.match(args.client_id) or args.client_id in 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()}"
if derived != args.case_ref:
raise Exit(2, 'case_ref_mismatch')
if spec.get('type') != CASE_TYPE:
raise Exit(2, 'case_type_mismatch')
if not isinstance(spec.get('type'), str):
raise Exit(2, 'case_type_unacknowledged')
if spec.get('status') != 'materialized':
raise Exit(2, 'case_not_materialized')
if params.get('client_type') != 'confidential':
raise Exit(2, 'case_client_not_confidential')
if params.get('client_id') != args.client_id:
raise Exit(2, 'case_client_id_mismatch')
check_acknowledged_case(doc)
receipt['case_verified'] = True

View file

@ -25,6 +25,20 @@ class CredentialGrantBindTests(unittest.TestCase):
grants.validate_grant(grant, index, catalog, 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:
catalog = grants.yaml.safe_load(
(REPO_DIR / "credential-grants/catalog.yaml").read_text()

View file

@ -6,6 +6,11 @@ token each call used: KV calls must use the minted grant token, never the
caller's session.
"""
import hashlib
import importlib.util
import shutil
from unittest import mock
import yaml
import json
import os
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):
(self.tmp / 'state.json').write_text(json.dumps(self.state))
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_extra or {}))
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):
for type_ref in ('oidc-browser-relying-party@1.0',
'oidc-browser-relying-party@1.2',
'oidc-browser-relying-party@1.3',
'oidc-browser-relying-party@99.0',
'confidential-service-client@1.1'):
with self.subTest(type_ref=type_ref):
self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref))
self.state['calls'] = []
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'], [])
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):
self.write_case(CASE.replace('status: materialized', 'status: accepted'))
code, receipt = self.run_cli()

View 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.