From 8540d674849d38e072789ac6aca1b0c622b022d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:07:07 +0900 Subject: [PATCH 1/5] fix(noema): consume stable OIDC response envelope --- .github/workflows/noema-review.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 59b25e343..b85e53c49 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -242,11 +242,21 @@ jobs: fail_unavailable "Noema app token exchange unavailable: app token request did not complete." fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + if ! jq -e --arg target_repository "$TARGET_REPOSITORY" ' + .ok == true + and (.data | type == "object") + and (.data.token | type == "string" and length > 0) + and .data.repository == $target_repository + and (.data.workflow_ref | type == "string" and length > 0) + and (.data.token_expires_at | type == "string" and length > 0) + ' >/dev/null <<<"$token_response"; then + fail_unavailable "Noema app token exchange unavailable: response envelope was invalid." + fi + + app_token="$(jq -r '.data.token' <<<"$token_response")" if [ -z "$app_token" ]; then fail_unavailable "Noema app token exchange unavailable: app token response was empty." fi - echo "::add-mask::$app_token" echo "token=$app_token" >>"$GITHUB_OUTPUT" From 6283581a40c0c8de22c6c276e7b281b68459e7c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:07:29 +0900 Subject: [PATCH 2/5] test(noema): bind OIDC success-envelope contract --- tests/test_noema_oidc_exchange_contract.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_noema_oidc_exchange_contract.py diff --git a/tests/test_noema_oidc_exchange_contract.py b/tests/test_noema_oidc_exchange_contract.py new file mode 100644 index 000000000..5e668d4c7 --- /dev/null +++ b/tests/test_noema_oidc_exchange_contract.py @@ -0,0 +1,44 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "noema-review.yml" + + +def workflow_step(workflow: str, name: str) -> str: + """Return one named workflow step without parsing untrusted YAML tags.""" + marker = f" - name: {name}\n" + start = workflow.index(marker) + try: + end = workflow.index("\n - name:", start + len(marker)) + except ValueError: + end = len(workflow) + return workflow[start:end] + + +def test_oidc_exchange_consumes_noema_standard_success_envelope() -> None: + """Require the central reviewer to consume Noema's stable data envelope.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") + + assert 'jq -r ".token // empty"' not in exchange + assert "Noema app token exchange unavailable: response envelope was invalid." in exchange + assert 'jq -e --arg target_repository "$TARGET_REPOSITORY"' in exchange + assert ".ok == true" in exchange + assert "(.data | type == \"object\")" in exchange + assert "(.data.token | type == \"string\" and length > 0)" in exchange + assert ".data.repository == $target_repository" in exchange + assert "(.data.workflow_ref | type == \"string\" and length > 0)" in exchange + assert "(.data.token_expires_at | type == \"string\" and length > 0)" in exchange + assert 'app_token="$(jq -r \'.data.token\' <<<"$token_response")"' in exchange + + +def test_oidc_exchange_keeps_token_out_of_diagnostics() -> None: + """Require envelope failures to avoid reflecting raw credential material.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") + + assert 'echo "$token_response"' not in exchange + assert 'printf "%s" "$token_response"' not in exchange + assert 'echo "::add-mask::$app_token"' in exchange + assert 'echo "token=$app_token" >>"$GITHUB_OUTPUT"' in exchange From 01270af66daccfa00237753a0229cdd1dd8913c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:08:05 +0900 Subject: [PATCH 3/5] docs(noema): record OIDC envelope interoperability RCA --- .../doctoring/noema-oidc-exchange-envelope.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/doctoring/noema-oidc-exchange-envelope.md diff --git a/docs/doctoring/noema-oidc-exchange-envelope.md b/docs/doctoring/noema-oidc-exchange-envelope.md new file mode 100644 index 000000000..1eaee6ec1 --- /dev/null +++ b/docs/doctoring/noema-oidc-exchange-envelope.md @@ -0,0 +1,64 @@ +# Noema OIDC exchange response-envelope contract + +검토 기준일: **2026-08-07** + +## 문제 + +중앙 `noema-review.yml`의 OIDC credential 경로는 Noema `/exchange` 성공 응답에서 top-level `.token`을 읽고 있었습니다. 그러나 Noema의 공개 API 안정성 계약은 성공 값을 다음과 같이 `data` object 아래에 둡니다. + +```json +{ + "ok": true, + "data": { + "token": "ghs_...", + "repository": "ContextualWisdomLab/example", + "workflow_ref": "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + "token_expires_at": "2026-08-07T12:00:00Z" + }, + "trace_id": "..." +} +``` + +따라서 provider가 token을 정상 발급해도 consumer가 `.token`을 조회하면 빈 값이 되어 중앙 reviewer가 항상 실패했습니다. 이 결함은 credential이 없는 것처럼 보이지만 실제 원인은 provider/consumer schema 불일치입니다. + +## 결정 + +OIDC consumer는 token field 하나만 permissive하게 조회하지 않고 다음 전체 contract를 fail closed로 검증합니다. + +1. top-level `ok`가 정확히 `true`여야 합니다. +2. `data`가 JSON object여야 합니다. +3. `data.token`이 비어 있지 않은 string이어야 합니다. +4. `data.repository`가 요청한 `TARGET_REPOSITORY`와 정확히 같아야 합니다. +5. `data.workflow_ref`가 비어 있지 않은 string이어야 합니다. +6. `data.token_expires_at`가 비어 있지 않은 string이어야 합니다. +7. 검증된 뒤에만 `data.token`을 추출하고 즉시 GitHub Actions mask를 적용합니다. +8. malformed response를 진단할 때 raw response나 token 값을 출력하지 않습니다. + +이 변경은 Noema의 reviewer App, PAT fallback, LLM provider, `NVIDIA_NIM_API_KEY`, repository permission 또는 merge authority를 변경하지 않습니다. OIDC path가 이미 발행된 stable response envelope를 정확히 소비하도록 고치는 interoperability repair입니다. + +## 표준 근거 + +RFC 8259는 JSON object를 name/value member의 집합으로 정의하고, member name이 고유할 때 구현 간 mapping agreement가 가능하다고 설명합니다. 또한 networked JSON text는 UTF-8을 사용해야 하며 parser가 size·depth·string length 제한을 둘 수 있음을 명시합니다. 이 변경은 shell의 loose field lookup 대신 object shape와 typed member를 명시적으로 검사하여 producer/consumer가 같은 mapping을 사용하도록 합니다. + +NIST SP 800-218 SSDF Version 1.1은 소프트웨어 생산자가 vulnerability의 근본 원인을 줄이고 소비자·구매자와 공통 보안 언어로 소통할 수 있도록 secure-development practices를 SDLC에 통합할 것을 권고합니다. 현재 finalized baseline은 v1.1이며, Rev. 1 / SSDF Version 1.2는 2025년 12월 공개된 initial public draft입니다. 이 변경은 실제 integration failure를 회귀 계약으로 고정하고 permissive fallback 대신 명시적 failure evidence를 남긴다는 점에서 해당 원칙을 적용합니다. + +## 회귀 계약 + +- workflow가 `.token // empty`를 사용하지 않습니다. +- `jq -e`가 stable envelope와 target repository를 검증합니다. +- 추출 경로는 `.data.token`입니다. +- malformed envelope는 `response envelope was invalid`로 실패합니다. +- raw response는 diagnostic output으로 반사하지 않습니다. +- token은 output 기록 전에 `::add-mask::` 처리됩니다. + +## 롤백과 호환성 + +롤백은 top-level `.token`으로 되돌리는 것이 아니라, provider의 실제 stable envelope가 변경되었다는 독립적으로 검증된 근거가 있을 때 producer와 consumer 계약을 같은 변경에서 함께 갱신하는 방식으로 수행합니다. 기존 GitHub App 및 PAT credential 경로는 이 OIDC schema repair와 독립적으로 유지되며, standalone product repositories는 중앙 reviewer의 내부 response parsing에 런타임 결합되지 않습니다. + +## References (APA 7th) + +Bray, T. (2017). *The JavaScript Object Notation (JSON) data interchange format* (RFC 8259). Internet Engineering Task Force. https://doi.org/10.17487/RFC8259 + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development Framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2025, December 17). *Secure Software Development Framework (SSDF) version 1.2 is available for public comment*. https://www.nist.gov/news-events/news/2025/12/secure-software-development-framework-ssdf-version-12-available-public From 97276da73c3b5d7312ce01eb88f8260ff75f3930 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:08:29 +0900 Subject: [PATCH 4/5] docs(noema): record OIDC envelope repair in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf30091dd..76c09ea84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Corrected the central Noema OIDC reviewer credential consumer to validate Noema's standard success envelope and extract only the repository-bound `data.token`, failing closed on malformed or mismatched metadata without reflecting raw token responses. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. - Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped. From 7e3c9e581f981bd2ceaa37c7b9bde16b452b899b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 08:37:13 +0900 Subject: [PATCH 5/5] test(noema): strengthen OIDC envelope contract --- tests/test_noema_oidc_exchange_contract.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_noema_oidc_exchange_contract.py b/tests/test_noema_oidc_exchange_contract.py index 5e668d4c7..71b7e727c 100644 --- a/tests/test_noema_oidc_exchange_contract.py +++ b/tests/test_noema_oidc_exchange_contract.py @@ -21,7 +21,7 @@ def test_oidc_exchange_consumes_noema_standard_success_envelope() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") - assert 'jq -r ".token // empty"' not in exchange + assert ".token // empty" not in exchange assert "Noema app token exchange unavailable: response envelope was invalid." in exchange assert 'jq -e --arg target_repository "$TARGET_REPOSITORY"' in exchange assert ".ok == true" in exchange @@ -40,5 +40,8 @@ def test_oidc_exchange_keeps_token_out_of_diagnostics() -> None: assert 'echo "$token_response"' not in exchange assert 'printf "%s" "$token_response"' not in exchange - assert 'echo "::add-mask::$app_token"' in exchange - assert 'echo "token=$app_token" >>"$GITHUB_OUTPUT"' in exchange + mask = 'echo "::add-mask::$app_token"' + output = 'echo "token=$app_token" >>"$GITHUB_OUTPUT"' + assert mask in exchange + assert output in exchange + assert exchange.index(mask) < exchange.index(output)