RPF-WP-0069-T08: broker admits oidc-service-client@0.2; CCR-2026-0060/0061 aligned to 0.2
The founder acknowledged oidc-service-client 0.2 on 2026-11-01 (memo r2, hub decision f8044f81, net-kingdom 9ce0d60), meeting the RPF-WP-0060-T01 rule from review R1. keycape_client_secret_generate.py pins the 0.2 type bytes, keys parameter pins by exact type reference, and derives confidentiality per type: browser 1.1 keeps its client_type check, the service type is confidential by construction (no client_type, unconditional custody path); a service case carrying client_type is refused. Create-only is unchanged. Eleven new tests; the existing browser tests are unchanged apart from the re-keyed pin. CCR-2026-0060/0061 match the 0.2 materialization; references updated, still proposed. T09 (wait) tracks their approval and apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
50e51e0fd8
commit
ba445d7aed
9 changed files with 364 additions and 42 deletions
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
"""RPF-WP-0054-T03: generate a KeyCape confidential-client secret inside custody.
|
||||
|
||||
Entry point for the oidc-browser-relying-party ``mint-secret`` step (lane
|
||||
``keycape-client-secret-generate``). Normally reached as
|
||||
Entry point for the ``mint-secret`` step (lane ``keycape-client-secret-generate``)
|
||||
of the acknowledged KeyCape client types: oidc-browser-relying-party@1.1 and
|
||||
oidc-service-client@0.2 (RPF-WP-0069-T08). Normally reached as
|
||||
``credential.py generate ...``; ops-warden runs it as a child in the caller's
|
||||
own environment and holds no token.
|
||||
|
||||
|
|
@ -16,8 +17,10 @@ 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
|
||||
an exactly source-pinned acknowledged browser revision; status ``materialized``; client_type
|
||||
``confidential``; ``parameters.client_id`` equals ``--client-id``, which
|
||||
an exactly source-pinned acknowledged revision; status ``materialized``; the
|
||||
client is confidential (browser 1.1: ``parameters.client_type``; service 0.2:
|
||||
derived from the authenticated type, which has no client_type and always
|
||||
holds custody); ``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
|
||||
``keycape-client-secret-generate``. All KV calls below use only that token.
|
||||
|
|
@ -65,6 +68,19 @@ ACKNOWLEDGED_TYPES = {
|
|||
'oidc-browser-relying-party@1.1': (
|
||||
'security-case-types/oidc-browser-relying-party/type.yaml',
|
||||
'f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c'),
|
||||
# Acknowledged 2026-11-01: founder accepted informed-decision memo
|
||||
# nk-wp-0056-t14-oidc-service-client-r2 (hub decision f8044f81; net-kingdom 9ce0d60).
|
||||
'oidc-service-client@0.2': (
|
||||
'security-case-types/oidc-service-client/type.yaml',
|
||||
'cd2386930027ef08f6fb135b2cfda72c355768b06e914cc74812af1bb214fe04'),
|
||||
}
|
||||
# How the broker establishes that the client is confidential. 'parameter': the case's
|
||||
# client_type must be confidential (browser 1.1, unchanged). 'type': the authenticated
|
||||
# type has no client_type parameter and its client-secret-path is unconditional, so
|
||||
# every case of it is confidential. A type not listed here keeps the parameter check.
|
||||
CONFIDENTIALITY = {
|
||||
'oidc-browser-relying-party@1.1': 'parameter',
|
||||
'oidc-service-client@0.2': 'type',
|
||||
}
|
||||
# Same authenticated producer and parameter projection as ops-warden case_inputs
|
||||
# at b593c879, restricted to this broker's currently admitted 1.1. Source-only
|
||||
|
|
@ -79,8 +95,10 @@ SOURCE_PINS = {
|
|||
'tools/playbook-capability-contract/playbook_contract_validator.py':
|
||||
'2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d',
|
||||
}
|
||||
# Keyed by the exact type reference, so two types sharing a version never share a pin.
|
||||
PARAMETER_PINS = {
|
||||
'1.1': '9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c',
|
||||
'oidc-browser-relying-party@1.1': '9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c',
|
||||
'oidc-service-client@0.2': 'dda3bd19f85369c3806abd78014b8fc3d400705d2f091b6256f02912aea51549',
|
||||
}
|
||||
_VALIDATE = """
|
||||
import json, sys
|
||||
|
|
@ -91,7 +109,7 @@ 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 pattern of oidc-browser-relying-party@1.1 and oidc-service-client@0.2 (identical).
|
||||
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}$')
|
||||
# 48 random bytes -> 64 URL-safe characters, confirmed by key-cape 2026-10-10
|
||||
|
|
@ -139,6 +157,15 @@ def authenticated_source(relative, expected):
|
|||
return raw
|
||||
|
||||
|
||||
def type_is_confidential(spec):
|
||||
# Derived from the authenticated type bytes, never from a case parameter.
|
||||
if any(p.get('name') == 'client_type' for p in spec['parameters']):
|
||||
return False
|
||||
paths = [m for m in spec['materialization'] if m.get('id') == 'client-secret-path']
|
||||
return (len(paths) == 1 and paths[0].get('kind') == 'kv_path' and 'when' not in paths[0]
|
||||
and paths[0].get('template') == 'platform/workloads/keycape/{client_id}')
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -155,8 +182,10 @@ def check_acknowledged_case(doc):
|
|||
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):
|
||||
or PARAMETER_PINS.get(doc['spec']['type']) != fingerprint):
|
||||
raise ValueError
|
||||
if CONFIDENTIALITY.get(doc['spec']['type']) == 'type' and not type_is_confidential(spec):
|
||||
raise Exit(2, 'case_type_not_confidential')
|
||||
with tempfile.TemporaryDirectory(prefix='platform-case-validator-') as directory:
|
||||
for relative, expected in SOURCE_PINS.items():
|
||||
raw = authenticated_source(relative, expected)
|
||||
|
|
@ -196,8 +225,12 @@ def check_case(args, receipt):
|
|||
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 CONFIDENTIALITY.get(spec['type'], 'parameter') == 'parameter':
|
||||
if params.get('client_type') != 'confidential':
|
||||
raise Exit(2, 'case_client_not_confidential')
|
||||
elif 'client_type' in params:
|
||||
# The service type decides confidentiality; a case cannot claim it.
|
||||
raise Exit(2, 'case_client_type_not_admitted')
|
||||
if params.get('client_id') != args.client_id:
|
||||
raise Exit(2, 'case_client_id_mismatch')
|
||||
check_acknowledged_case(doc)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue