diff --git a/docs/keycape-client-secret-generate-type-pin-review.md b/docs/keycape-client-secret-generate-type-pin-review.md new file mode 100644 index 0000000..5ca47d3 --- /dev/null +++ b/docs/keycape-client-secret-generate-type-pin-review.md @@ -0,0 +1,30 @@ +# Exact case-type pin correction — 2026-10-10 + +The broker's `CASE_TYPE` still required browser-RP @1.0 after the founder +authorized @1.1 and net-kingdom re-pinned the accepted Forgejo case. That made +the real case fail `case_type_mismatch` before any backend call. + +`keycape_client_secret_generate.py` now accepts exactly +`oidc-browser-relying-party@1.1`; @1.0, arbitrary later versions and other +types remain refused. Existing exact-byte case-ref, materialized status, +client match, confidential/reserved identity, caller/root/agent boundary, +scoped grant, CAS0 create-once, silent receipt and revocation checks are unchanged. + +Authority evidence: NK-WP-0048, 2026-10-10 founder go for 1.1; source type +`security-case-types/oidc-browser-relying-party/type.yaml` version 1.1, +acknowledged; accepted Forgejo case carries @1.1. No case status, live backend +or credential changed during this correction. + +Regression uses the actual accepted Forgejo file shape with materialized +status solely in the offline fake test fixture. Real accepted (unmaterialized) +case remains refused. Ten subprocess CLI tests pass against a fake bao in an +isolated temporary tree, including silence, exact mismatch before any backend +call, reserved/agent/root identity guards, scoped token delivery, CAS0/race and +revocation. No live broker execution is claimed. + +The materialization/run circularity remains an owner contract question: +MASON-WP-0010 describes mint-secret as needed before materialized, while +WARDEN-WP-0044 and this broker require materialized before mint-secret. +Owner review should define structural materialization with exact applied reader +receipts, then follow-through creates custody and verifies delivery. This +correction does not silently relax status gating or assert those receipts. diff --git a/scripts/keycape_client_secret_generate.py b/scripts/keycape_client_secret_generate.py index 25bacdb..207bf7d 100644 --- a/scripts/keycape_client_secret_generate.py +++ b/scripts/keycape_client_secret_generate.py @@ -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.0``; status ``materialized``; client_type + ``oidc-browser-relying-party@1.1``; 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 @@ -57,8 +57,8 @@ 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.0' -# oidc-browser-relying-party@1.0 parameter client_id. +CASE_TYPE = 'oidc-browser-relying-party@1.1' +# 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}$') # 48 random bytes -> 64 URL-safe characters, confirmed by key-cape 2026-10-10 diff --git a/tests/test_keycape_client_secret_generate.py b/tests/test_keycape_client_secret_generate.py index b9a51f0..428424a 100644 --- a/tests/test_keycape_client_secret_generate.py +++ b/tests/test_keycape_client_secret_generate.py @@ -53,17 +53,8 @@ FAKE_BAO = textwrap.dedent(f'''\ save(); sys.exit(9) ''') -CASE = textwrap.dedent('''\ - apiVersion: netkingdom.io/security-case/v0.1 - kind: SecurityCase - spec: - id: forgejo-netkingdom-oidc - type: oidc-browser-relying-party@1.0 - status: materialized - parameters: - client_id: forgejo - client_type: confidential -''') +# Exact accepted Forgejo @1.1 shape; materialized only in this fake test. +CASE = '# First KnownSecurityCase of oidc-browser-relying-party 1.1 (NK-WP-0048-T06).\n# Parameters are the request the founder accepted on 2026-10-08\n# (key-cape docs/forgejo-oidc-client-request.yaml, KEY-WP-0038-T01); that\n# acceptance carries over as this case\'s acceptance. One difference: custody is\n# derived by the type (platform/workloads/keycape/forgejo), not the request\'s\n# proposed platform/workloads/forgejo/netkingdom-oidc-client. The founder\n# acknowledged that derivation with the type (memo r4, 2026-10-09).\n# Re-pinned from 1.0 to 1.1 on 2026-10-10 on the founder\'s go, before any\n# apply, so no 1.0 run overlaps (minor change: consumer-enforces-groups).\napiVersion: netkingdom.io/security-case/v0.1\nkind: SecurityCase\nspec:\n id: forgejo-netkingdom-oidc\n type: oidc-browser-relying-party@1.1\n status: materialized\n acceptance:\n at: "2026-10-08"\n record: "key-cape docs/forgejo-oidc-client-request.yaml (status accepted-2026-10-08, KEY-WP-0038-T01); type acknowledged via informed-decision memo nk-wp-0048-t05-oidc-browser-relying-party-r4, hub decision e418263e"\n by:\n - {role: owner, name: "founder (for railiance-apps)"}\n - {role: founder, name: founder}\n owner:\n repo: railiance-apps\n person: founder\n parameters:\n client_id: forgejo\n display_name: Forgejo source forge\n redirect_uris: https://forgejo.coulomb.social/user/oauth2/netkingdom/callback\n allowed_scopes: openid,profile,email,groups\n client_type: confidential\n mfa_required: "true"\n access_groups: forge-admins,forge-coulomb-owners,forge-coulomb-readers\n initial_members: forge-admins=tegwick;forge-coulomb-owners=tegwick\n consuming_service: forgejo\n consumer_owner_repo: railiance-apps\n' class Generate(unittest.TestCase): @@ -155,6 +146,24 @@ class Generate(unittest.TestCase): code, receipt = self.run_cli() self.assertEqual((code, receipt['outcome']), (2, outcome)) + 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@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(self.state['calls'], []) + + def test_exact_accepted_case_still_requires_materialization(self): + self.write_case(CASE.replace('status: materialized', 'status: accepted')) + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'case_not_materialized')) + self.assertEqual(self.state['calls'], []) + def test_agents_root_and_unbound_callers_refused(self): code, receipt = self.run_cli(env_extra={'WARDEN_AGENT_ID': 'codex'}) self.assertEqual((code, receipt['outcome']), (2, 'agent_callers_refused'))