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
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue