Skip to content

security(keyverse): validate exact operational identity across the adapter boundary #296

Open

Description

@seonghobae

Finding

The Keyverse adapter has one shared operational-identity integrity invariant but two canonical writer lanes.

Do not duplicate mutable source between them. Keyverse remains identity/authentication authority; Orgmetra remains HR policy/evidence authority.

#295 — binding owner

Current exact head: 086fe88607b1e30f72633b5222fbcaf8f846e9ca on protected develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f, open / Draft / mechanically mergeable.

The binding lane now rejects non-UUID and reserved/forged UUID identities, detaches caller-owned UUID aliases, requires exact canonical issuer/subject text, treats optional claim metadata as inert exact built-in names, rejects credential-shaped fields before trust classification, and exposes only non-authorizing ExternalIdentityBindingCandidate data.

Fresh audit also found that the preceding frozen-slots dataclass could be rewritten after successful validation via low-level object.__setattr__. Test-first 634daefaad125b6de49bac4e0905dec1f822ccf3 requires post-validation replacement to fail; its hosted jobs were cancelled by successor publication before execution, so it is not claimed as hosted RED. Ordinary successor 086fe886... uses tuple-backed structurally immutable retained storage while preserving the public keyword constructor and field properties.

Structural immutability is not trust provenance. Low-level construction can still manufacture a Python value, so any consequential consumer must reconstruct/revalidate exact owner evidence rather than infer authentication from object type. Positive durable binding remains unavailable until Keyverse publishes the released contract tracked in Keyverse#155 / Orgmetra#297.

Fresh Foundation 34540431536, SAST 34540431586, Security 34540431607, and CodeQL 34540431529 generations are currently queued for exact 086fe886...; Foundation job 103081629173 has runner_id: 0, empty runner identity and no steps. Queue-health handoff is .github#712 comment 5626634340. No predecessor result is promoted to this head.

#65 — authorization owner

#65 remains exact b7f2b730b8b231aa28e485c655fd8cdf18007d46, open / Draft / mechanically mergeable, on the same protected base. It owns authorization.py, policy/request detachment and revalidation, coherent AuthorizationDecision evidence, and durable-consumer authorization checks.

Its prerequisite order remains #63 shared HRIS-kernel protected integration → #64 People mutation protected integration → #65 non-force protected adoption/reconciliation. #295 must not absorb this source simply to make this issue appear complete.

Acceptance

  • fix(keyverse): fail closed without authenticated identity authority #295 exact-current-head preserves exact operational UUID, exact/canonical text, inert claim-name, credential-rejection and post-validation structural-integrity invariants at repository coverage targets.
  • fix(authz): protect purpose-bound runtime integrity #65 exact-current-head preserves equivalent or stronger operational-identity integrity at authorization construction, evaluation and consequential consumers.
  • Future binding consumers revalidate/reconstruct released trust evidence; locally forgeable language objects are never authorization/authentication authority.
  • Applicable exact-head repository/security/review gates pass without weakening manifests, workflows or owner boundaries.
  • Both canonical lanes reach normal protected integration before this issue is completed.

No force-push, destructive rebase, self/model approval, routine admin bypass, gate weakening, mutable-owner source copy, predecessor-verdict transfer, empty retrigger commit, or simple Close is authorized.

Activity

  1. changed the title [-]security(keyverse): validate exact operational identity before persisting a subject binding[/-] [+]security(keyverse): validate exact operational identity across the adapter boundary[/+] on Sep 10, 2026
  2. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Fresh owner split refinement: Issue #297 now owns a separate authentication authority/provenance gap above #296's operational-identity integrity.

    #295 test-first head 97e12be2f3131e9ecbdeec99218c12f45705385b produced a real Foundation RED (34513085296/102991763652) after tests stopped accepting raw caller issuer/subject as durable binding authority. Current #295 head d7b7f531e216027a99efe31e36dc1aab60fe23af makes raw inputs only ExternalIdentityBindingCandidate data and makes bind_identity_subject(...) fail closed until an immutable released/versioned Keyverse subject-assertion contract exists; Foundation 34513172553/102992051252 is GREEN.

    This does not replace #296: exact UUID/text integrity and alias detachment still belong here, split across #295 binding and #65 authorization. #297 additionally prevents syntactically valid values from laundering caller assertion into authentication truth. Both issues remain open; no local trusted boolean/receipt or mutable Keyverse source copy is an acceptable completion.

  3. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Current owner reconciliation: #295 binding/ACL lane is now exact d15a741c10685208b124477e9d7820cdf17339e8, Draft and mechanically mergeable on protected develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f. Its current diff has five files after the fail-closed public boundary, CHANGELOG reconciliation, and deterministic manifest reseal; do not treat the earlier four-file snapshot as current authority.

    Single-writer split remains unchanged: #295 owns Keyverse identity-subject binding/ACL integrity; #65 owns purpose-bound authorization internals/runtime-integrity. #295 must not reintroduce authorization.py or its tests, and #65 must not absorb mutable #295 binding source. The manifest collateral RED introduced at cf9d6d1... was repaired by d15a741...; exact-head Foundation 34515073682/102998336570 is GREEN.

    #296 remains open until both owner lanes integrate normally through protected develop and downstream durable consumers reacquire exact protected-head evidence. Issue #297 separately tracks the absent immutable Keyverse subject-assertion release required for any future positive durable-binding path.

  4. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    State-only currentization after the prior owner note: #295 was subsequently marked Ready for Review without changing head/base and remains exact d15a741c10685208b124477e9d7820cdf17339e8 on protected develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f. This does not alter the single-writer split or completion boundary. Repository-owned Foundation/SAST/Security evidence is current-head GREEN, but CodeQL is non-passing on the central producer/consumer settlement defect, Noema/Strix remain independently pending at the last read, and there is still no qualifying independent APPROVED. #296 therefore remains open; no authorization-source overlap or mutable sibling adoption is authorized.

  5. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Fresh #295 binding-owner extension on top of d15a741c10685208b124477e9d7820cdf17339e8 found the same runtime-integrity class in optional claim-name screening.

    Protected/current binding code used claims = extra_claims or {} and then name.lower() over caller-provided keys without first requiring an exact built-in dict and exact built-in string keys. A dict subtype can therefore execute caller-defined truthiness before the trust gate; a str subtype key can execute caller-defined lower(), and a non-text key leaks an incidental AttributeError rather than the bounded validation contract. This does not claim sandboxing against arbitrary code already running in-process; it removes behavior-bearing values from this specific trust-boundary validation path.

    Test-first 85c08ac7e991877171d44dd523aaad585c599259 adds regressions for executable dict/key subtypes and malformed non-text claim names. Its hosted workflows were superseded/cancelled before a test result, so it is not claimed as hosted RED; the predecessor source behavior itself reproduces the unbounded calls. Ordinary-forward source repair a3fa3470b6b4b489889b574afcc1f672a7fdedcc adds _validate_extra_claim_names: None or exact built-in dict only, exact built-in string keys only, then credential-name comparison; claim values remain uninterpreted/unretained. Existing credential-name rejection semantics remain intact.

    #295 has been returned to Draft while exact-head hosted evidence is reacquired. #65 remains the authorization writer; no authorization source moved back into #295.

  6. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Exact-current-head GREEN recovered for the new claim-name repair: Foundation CI 34518363609/103009403837 on a3fa3470b6b4b489889b574afcc1f672a7fdedcc completed SUCCESS. Exact checkout, Python compilation, foundation-pack/dependency-hygiene validation, owned unit/service contracts (including the new executable-container/key regressions), isolated PostgreSQL contracts, and read-only validation all passed. SAST/Security/CodeQL are still independently queued at this observation; no predecessor result is transferred.

  7. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Fresh #295 binding-owner finding and repair, ordinary-forward on protected develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f.

    Exact predecessor a3fa3470b6b4b489889b574afcc1f672a7fdedcc already required extra_claims to be an exact built-in dict with exact built-in string keys, but it did not require those keys to be canonical. Consequently an exact key such as " Access_Token " did not intersect the forbidden credential-name set after lower() (the surrounding whitespace remained), and a blank exact string key was accepted. This is a binding-owned input-integrity defect; it does not move authentication authority from Keyverse or authorization ownership from #65.

    Test-first b63c50a88f50dbeb1ea1a23eb58e781692f8427a added padded credential-name and blank-name regressions. Foundation 34530360487/103049319749 is a real hosted RED on that exact test-only head: the Keyverse adapter ran 53 tests, with both new cases failing because predecessor code reached IdentityBindingTrustUnavailableError instead of rejecting the malformed claim name; 51 existing tests passed and owned keyverse-adapter production coverage remained 208 statements / 70 branches at 100%. The test-only head was then superseded normally.

    Minimal source successor 694585a64d038d30e851e156b8d5423e567c14f8 reuses _validate_canonical_text("extra claim name", name) inside _validate_extra_claim_names() before credential-name case folding. Exact built-in dict/type checks remain; claim values remain uninterpreted and unretained. #295 was returned to Draft before these source changes and remains the sole binding/ACL writer; #65 remains the authorization writer.

    Current exact-head Foundation/SAST/Security/CodeQL workflows have materialized but are still queued at this read, so no GREEN is claimed yet. Keep #296 open until both canonical owner lanes integrate normally.

  8. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Follow-up repair after the first source successor: exact 694585a64d038d30e851e156b8d5423e567c14f8 correctly made the two new canonical-name regressions pass, but Foundation 34530404135/103049459129 exposed a compatibility RED in two pre-existing binding tests. The reused generic text validator changed the established bounded malformed-key error from extra claim names must be exact text to the singular extra claim name must be exact text, so test_extra_claims_reject_executable_container_or_key_before_use and test_extra_claims_reject_non_text_claim_name_with_bounded_error failed. The same run showed the two new canonical-name tests passing and 100% production statement/branch coverage.

    Minimal ordinary-forward successor 625649e1770e395e3e6f44b36235d363ebf168ea preserves the existing exact-type error contract with an explicit type(name) is str gate, then delegates only exact strings to _validate_canonical_text("extra claim name", name) for blank/padded canonicality. This keeps executable string subtypes rejected before method use while adding canonical-name screening without changing the prior bounded error surface. #295 remains Draft; fresh exact-head workflows have materialized and are incomplete, so GREEN is not yet claimed.

  9. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Current exact successor 625649e1770e395e3e6f44b36235d363ebf168ea has now closed the repository-owned RED: Foundation 34530912986/103051125781 completed SUCCESS on the exact checkout. The Keyverse adapter ran 53/53 tests including both new canonical-name regressions and the two pre-existing bounded malformed-key error tests; owned adapter production coverage is 208 statements / 70 branches at 100%. The same Foundation run passed compilation, foundation/dependency hygiene, all other owned unit/service suites, isolated PostgreSQL contracts, and read-only validation. Manifest repair output was correctly skipped because this latest source/test delta is outside the deterministic manifest-tracked paths.

    #295 remains Draft while current-head SAST 34530913196, Security 34530913081, and CodeQL 34530913154 complete. Do not transfer predecessor security/review evidence.

  10. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Fresh #295 binding-owner hardening, preserving the #295/#65 single-writer split.

    Current #295 exact head is da7d005e87c906f0ed1f0e8bd91c0677b3fff5d9 on unchanged protected develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f, Draft while successor checks settle. Review of the exact predecessor found that credential-name screening rejected access_token/refresh_token but still allowed raw id_token, client_secret, and api_key fields to reach the missing-owner-trust path as ordinary extra claims. That contradicts the adapter's no-raw-credentials boundary; it does not require Orgmetra to verify those credentials.

    • test-first b15a3511c8b4eb9ad8acf8841d43a868ef253370 adds regressions requiring those standard credential fields to fail before trust classification;
    • its Foundation run 34535505235 was cancelled by the ordinary source successor before runner acquisition, so it is not represented as hosted RED;
    • minimal source da7d005e87c906f0ed1f0e8bd91c0677b3fff5d9 only extends the existing forbidden-name set with id_token, client_secret, and api_key;
    • claim values remain uninterpreted/unretained, positive binding remains unavailable, and fix(authz): protect purpose-bound runtime integrity #65 authorization source/tests are untouched.

    Keep this issue open. Current-head Foundation/SAST/Security/CodeQL generations are fresh but queued at the latest read; no predecessor result transfers.

  11. seonghobae commented on Sep 10, 2026

    @seonghobae
    ContributorAuthor

    Binding-owner update: #295 advanced ordinary-forward from da7d005e87c906f0ed1f0e8bd91c0677b3fff5d9 through test-first 634daefaad125b6de49bac4e0905dec1f822ccf3 to source successor 086fe88607b1e30f72633b5222fbcaf8f846e9ca on the unchanged protected base develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f.

    Fresh audit found the exported non-authorizing ExternalIdentityBindingCandidate was still a frozen slots dataclass. Python's low-level object.__setattr__ can replace an existing frozen slot after __post_init__, so a caller retaining the returned candidate could rewrite identity_subject after validation. This is a validated-data integrity defect, not an authentication bypass: the candidate remains non-authorizing and there is still no positive durable-bind path.

    634daefa... adds a regression that a post-validation attribute-replacement attempt must fail and the subject remain unchanged. Its hosted runs were cancelled by the ordinary source successor before execution, so it is not claimed as hosted RED. 086fe886... moves candidate storage to tuple-backed immutable storage, preserves the keyword constructor and public field properties, and retains exact UUID/text validation. The implementation explicitly does not treat structural immutability or Python object type as trust provenance; any future consequential consumer must reconstruct/revalidate authoritative evidence.

    #65 remains the authorization single writer. Do not copy this storage change into authorization or mark #296 complete until both canonical lanes reach protected integration. Fresh exact-head Foundation/SAST/Security/CodeQL generations for 086fe886... are still settling.

  12. seonghobae commented on Sep 11, 2026

    @seonghobae
    ContributorAuthor

    Current owner update after fresh #295 review: CodeRabbit found a valid remaining alias defect on former exact head 086fe88607b1e30f72633b5222fbcaf8f846e9ca. Tuple-backed storage still retained tenant/person as mutable UUID objects and returned those same objects, so caller mutation of a returned UUID .int could change later candidate reads.

    #295 was returned to Draft. Test-first 7d44147ef005508c3e071d6dec10c13f68846e45 adds tenant/person returned-view mutation regressions; its hosted runs were cancelled by ordinary successor publication before execution and are not claimed as hosted RED. Minimal successor c7d39a6702ed2149c79dc2aa7c2c4be983679f9c retains only checked integer scalars and returns fresh UUID(int=...) views. Exact-head Foundation/SAST/Security/CodeQL are currently queued, and the live CodeRabbit thread remains unresolved until GREEN.

    Single-writer split is unchanged: #295 owns binding/candidate ACL; #65 owns purpose-bound authorization. Structural integrity still is not Keyverse authentication provenance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions