-
Notifications
You must be signed in to change notification settings - Fork 0
fix(auth): normalize bearer-token comparison to fixed-size digests #790
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
6
commits into
develop
Choose a base branch
from
sentinel-hmac-timing-leak-454043180780305011
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6d2c61d
보안 취약점: hmac.compare_digest 타이밍 기반 길이 유출 수정
seonghobae faedf8d
test(auth): require fixed-size token comparison
seonghobae 1d4e61d
fix(auth): precompute fixed-size token digest
seonghobae 139c844
fix(auth): compare fixed-size token digests
seonghobae e35b37e
docs(auth): correct token timing boundary rationale
seonghobae 97e821d
docs(gap): baseline bearer comparison contract
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Product / Technical Gap Baseline | ||
|
|
||
| 이 문서는 NewsDOM API의 상용화 Gap을 live code, protected branch, 열린 PR, 실행 테스트와 공식 upstream 계약을 기준으로 관리한다. 서로 다른 PR의 아직 병합되지 않은 변경은 현재 branch의 기능으로 간주하지 않는다. | ||
|
|
||
| ## Bearer authentication comparison boundary | ||
|
|
||
| ### 문제 | ||
|
|
||
| `hmac.compare_digest()`는 내용 기반 short-circuit을 피하지만 Python 공식 문서는 두 operand의 길이가 다르면 type·length 정보가 이론적으로 timing을 통해 드러날 수 있다고 명시한다. 기존 branch의 `len(credentials) != len(expected_token)` 뒤 `compare_digest(credentials, credentials)` 방식은 configured token과 presented credential의 비교 길이를 정규화하지 않는다. caller가 자신의 입력 길이만큼 self-comparison을 한 뒤 실패할 뿐이다. | ||
|
|
||
| ### 제약과 선택 | ||
|
|
||
| - Authorization header 전체는 `MAX_BEARER_HEADER_BYTES=4096`에서 먼저 제한한다. | ||
| - configured token은 immutable `RuntimeSettings` 생성 시 UTF-8 정규화·길이 검증을 끝낸 뒤 SHA-256 digest를 한 번 계산한다. secret length에 따른 hash 작업을 request path에서 반복하지 않는다. | ||
| - request credential은 이미 제한된 bytes를 SHA-256으로 digest한다. | ||
| - 실제 equality check는 두 개의 고정 32-byte digest에 대해 `hmac.compare_digest()`를 정확히 한 번 수행한다. | ||
| - 이 경계는 wall-clock 시간이 완전히 동일하다고 주장하지 않는다. HTTP stack, hashing, scheduling 등 전체 request latency에는 변동이 있으므로 acceptance는 operand contract와 인증 semantics를 실행 테스트로 검증한다. | ||
|
|
||
| ### RED → GREEN evidence | ||
|
|
||
| - RED `faedf8d9103f8a510b35b49570124071cc875a36`: runtime settings가 fixed-size token digest를 미리 보유하고, 길이가 다른 presented token도 `compare_digest()`에 같은 digest 길이로 들어가며, 정확한 configured token은 인증 경계를 통과해야 한다는 regression을 추가했다. | ||
| - GREEN `1d4e61de2a879561cfeeb7871c1d01cc37114fad`: configured token digest를 immutable runtime configuration에서 사전 계산한다. | ||
| - GREEN `139c8440b894690a2f0d6dea2d183409bbf5361f`: request credential을 SHA-256 digest로 정규화하고 fixed-size digest끼리 비교한다. | ||
| - Documentation repair `e35b37e858b8fafd272d1428b5bceb97e16ae45c`: `credentials` self-comparison이 constant-time 보장을 만든다는 이전 설명을 제거하고 실제 boundary와 Python 문서의 제한을 기록한다. | ||
|
|
||
| ### 현재 acceptance | ||
|
|
||
| 현재 branch의 exact head에서 authentication tests, full repository tests, lint/type checks와 security checks가 terminal GREEN이어야 한다. 단순히 서로 다른 길이의 token이 401을 반환한다는 사실은 timing mitigation 증거가 아니다. predecessor head의 checks, source-neutral retrigger, self-approval, scanner suppression, required-gate 완화는 acceptance가 아니다. | ||
|
|
||
| ### 남은 Gap | ||
|
|
||
| 현재 bootstrap bearer token은 단일 shared secret이다. 다중 주체·회전·폐기·감사·권한 범위가 필요한 상용 identity 계약은 NewsDOM 내부 source copy나 별도 user store로 확장하지 않고 CWL canonical identity owner인 Keyverse의 released contract를 소비하는 ADR로 분리해야 한다. | ||
|
|
||
| ## Traceability | ||
|
|
||
| Python Software Foundation. (2026). *hmac — Keyed-hashing for message authentication*. Python 3.14.7 documentation. Retrieved September 3, 2026, from https://docs.python.org/3/library/hmac.html |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,3 +407,16 @@ def test_config_module_exposes_versioned_environment_contract() -> None: | |
| assert config.API_TOKEN_ENV_VAR == "NEWSDOM_API_TOKEN" | ||
| assert config.AUTH_MODE_ENV_VAR == "NEWSDOM_AUTH_MODE" | ||
| assert config.RUNTIME_PROFILE_ENV_VAR == "NEWSDOM_RUNTIME_PROFILE" | ||
|
|
||
| def test_authentication_constant_time_comparison_differing_lengths(): | ||
| '''Verify that tokens of different lengths do not leak length information.''' | ||
| settings = RuntimeSettings(api_token="valid_token") | ||
| app = create_app(settings) | ||
| client = TestClient(app) | ||
|
|
||
| response = client.post( | ||
| "/parse", | ||
| headers={"Authorization": "Bearer too_short"}, | ||
| files={"file": ("test.pdf", b"%PDF-1.4\n", "application/pdf")}, | ||
| ) | ||
| assert response.status_code == 401 | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+411
to
+422
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| """Regression coverage for fixed-size authentication token comparison.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import hmac | ||
|
|
||
| from fastapi.testclient import TestClient | ||
|
|
||
| import newsdom_api.main as main_module | ||
|
|
||
| from newsdom_api.config import RuntimeSettings | ||
| from newsdom_api.main import create_app | ||
|
|
||
|
|
||
| def test_runtime_settings_precomputes_fixed_size_token_digest() -> None: | ||
| """The configured secret is normalized once outside the request path.""" | ||
|
|
||
| settings = RuntimeSettings(api_token="configured-token") | ||
|
|
||
| assert settings.api_token_digest == hashlib.sha256(b"configured-token").digest() | ||
| assert len(settings.api_token_digest or b"") == hashlib.sha256().digest_size | ||
|
|
||
|
|
||
| def test_mismatched_token_uses_equal_length_digest_operands(monkeypatch) -> None: | ||
| """Request comparison never invokes compare_digest with secret-dependent lengths.""" | ||
|
|
||
| original_compare_digest = hmac.compare_digest | ||
| operands: list[tuple[bytes, bytes]] = [] | ||
|
|
||
| def record_compare_digest(left: bytes, right: bytes) -> bool: | ||
| operands.append((left, right)) | ||
| return original_compare_digest(left, right) | ||
|
|
||
| monkeypatch.setattr(main_module.hmac, "compare_digest", record_compare_digest) | ||
| client = TestClient(create_app(RuntimeSettings(api_token="configured-token"))) | ||
|
|
||
| response = client.post( | ||
| "/parse", | ||
| headers={"Authorization": "Bearer x"}, | ||
| files={"file": ("test.pdf", b"%PDF-1.4\n", "application/pdf")}, | ||
| ) | ||
|
|
||
| assert response.status_code == 401 | ||
| assert len(operands) == 1 | ||
| left, right = operands[0] | ||
| assert len(left) == len(right) == hashlib.sha256().digest_size | ||
|
|
||
|
|
||
| def test_valid_token_still_authenticates_after_digest_normalization(monkeypatch) -> None: | ||
| """Fixed-size comparison preserves exact configured-token acceptance.""" | ||
|
|
||
| monkeypatch.setattr(main_module, "parse_pdf", lambda *args, **kwargs: {"nodes": []}) | ||
| settings = RuntimeSettings(api_token="configured-token") | ||
| client = TestClient(create_app(settings, runtime_readiness_probe=lambda: True)) | ||
|
|
||
| response = client.post( | ||
| "/parse", | ||
| headers={"Authorization": "Bearer configured-token"}, | ||
| files={"file": ("test.pdf", b"not-used", "application/pdf")}, | ||
| ) | ||
|
|
||
| # Authentication must not reject the exact token. The intentionally invalid | ||
| # PDF may fail later at the media boundary, which is sufficient for this test. | ||
| assert response.status_code != 401 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 필수 품질 게이트 미검증
현재 환경에
uv와pytest가 없어 전체 테스트와 100% 분기 커버리지를 실행하지 못했다. CI 결과를 확인해야 한다.Was this helpful? React with 👍 or 👎 to provide feedback.