-
Notifications
You must be signed in to change notification settings - Fork 1
feat(tools): add bounded content checksum generator #1361
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
52
commits into
autoresearch/frontend-sec-bump
Choose a base branch
from
feat/content-checksum-generator
base: autoresearch/frontend-sec-bump
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
52 commits
Select commit
Hold shift + click to select a range
ca3ce5f
test(tools): define secure content checksum contract
seonghobae 4c794ad
feat(tools): implement bounded content checksum generator
seonghobae cf293ea
feat(tools): register content checksum generator
seonghobae a05de81
test(tools): cover checksum registration lifecycle
seonghobae d0e5639
docs(doctoring): record checksum standards and scope
seonghobae e1b559e
docs(adr): bound customer checksum algorithm surface
seonghobae a514f31
docs(adr): index checksum surface decision
seonghobae 5e5448a
docs(operations): document checksum customer workflow
seonghobae b3d0bc5
docs(adr): avoid concurrent ADR numbering collision
seonghobae c3057cd
docs(adr): reserve checksum decision as ADR-0007
seonghobae 2898533
docs(operations): point checksum workflow to ADR-0007
seonghobae d5245e9
docs(adr): remove conflicting ADR-0004 path
seonghobae 1371f9a
docs(doctoring): ground checksum choices in current standards and res…
seonghobae 3e6f791
docs(operations): specify authenticated checksum endpoint contract
seonghobae fce902f
test(tools): use application bootstrap import explicitly
seonghobae 5a39a07
docs(checksum): make coverage acceptance explicit
seonghobae fdec2e4
test(tools): cover checksum API contract
seonghobae 5816a82
test(tools): align checksum API success contract
seonghobae 08698f4
test(tools): exercise checksum API auth contract
seonghobae 142360c
test(tools): require forged-session rejection and stable checksum err…
seonghobae 6fce51e
fix(tools): attach deterministic checksum validation codes
seonghobae d914835
fix(tools): surface stable error codes in execution envelope
seonghobae 9c2fb7b
test(tools): consolidate checksum API contract coverage
seonghobae 046bb0c
test(security): reject missing CSRF provenance
seonghobae d269d50
fix(security): fail closed on missing CSRF provenance
seonghobae b454485
fix(api): preserve tool response compatibility
seonghobae 95a8c7a
fix(api): preserve null result while omitting absent error code
seonghobae 7902fb6
test(security): distinguish browser CSRF from API clients
seonghobae 77fd3c2
fix(security): scope CSRF provenance to browser requests
seonghobae c526df1
Merge branch 'develop' into feat/content-checksum-generator
seonghobae 5728524
Merge branch 'develop' into feat/content-checksum-generator
seonghobae c046ef0
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae 3aede33
Merge branch 'develop' into feat/content-checksum-generator
opencode-agent[bot] 7071b67
Merge remote-tracking branch 'refs/remotes/origin/develop' into codex…
seonghobae 85678dc
style: format checksum API coverage
seonghobae 5859a8f
test(checksum): import application symbol explicitly
seonghobae dd598cd
Merge branch 'develop' into feat/content-checksum-generator
seonghobae 18d1a15
repair(checksum): adopt current protected develop ancestry
seonghobae 90988f6
test(checksum): reject non-UTF-8 Unicode scalar input
seonghobae 228553b
test(checksum): expose invalid UTF-8 as stable API failure
seonghobae dc480c3
fix(checksum): fail closed on invalid UTF-8 scalar input
seonghobae d6f8b31
docs(checksum): refresh NIST status evidence
seonghobae 5150957
test(checksum): prove incremental chunk equivalence
seonghobae 030e870
docs(adr): keep checksum decision proposed until integration
seonghobae 18f14de
docs(adr): align checksum index with proposed state
seonghobae 1771b9d
docs(checksum): record chunk-equivalence evidence
seonghobae 165cfa9
chore(stack): adopt current frontend security owner into checksum lane
seonghobae 06d1239
Restack content-checksum owner onto current dependency-security parent
seonghobae c7a2670
refactor(checksum): hand browser provenance CSRF delta to #1706
seonghobae b98dbf9
refactor(checksum): remove CSRF regression now owned by #1706
seonghobae 5c42f52
test(checksum): adopt empty-input regression from generated duplicate…
seonghobae 6bf2989
test(checksum): strengthen published-vector and UTF-8 chunk evidence
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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """Bounded cryptographic checksums for exact UTF-8 text content. | ||
|
|
||
| This module owns the checksum algorithm allowlist and registers the tool with | ||
| Naruon's existing deterministic tool catalog. Its digests compare content | ||
| bytes; they are not proof of sender identity or message authenticity. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| from collections.abc import Callable | ||
| from typing import Any | ||
|
|
||
| from api.tools import ToolInfo, registry | ||
|
|
||
| MAX_CONTENT_BYTES = 1_048_576 | ||
| SECURITY_NOTE = ( | ||
| "Use this digest to compare exact content bytes; it does not authenticate " | ||
| "the sender or replace a MAC/signature." | ||
| ) | ||
|
|
||
|
|
||
| class ContentChecksumError(ValueError): | ||
| """Expected checksum validation failure with a stable machine error code.""" | ||
|
|
||
| def __init__(self, message: str, *, error_code: str) -> None: | ||
| """Initialize a customer-safe validation failure and deterministic code.""" | ||
| super().__init__(message) | ||
| self.error_code = error_code | ||
|
|
||
|
|
||
| def _sha256(payload: bytes) -> str: | ||
| """Return the SHA-256 hexadecimal digest for ``payload``.""" | ||
| return hashlib.sha256(payload).hexdigest() | ||
|
|
||
|
|
||
| def _sha3_256(payload: bytes) -> str: | ||
| """Return the SHA-3-256 hexadecimal digest for ``payload``.""" | ||
| return hashlib.sha3_256(payload).hexdigest() | ||
|
|
||
|
|
||
| def _blake2b_256(payload: bytes) -> str: | ||
| """Return the 256-bit BLAKE2b hexadecimal digest for ``payload``.""" | ||
| return hashlib.blake2b(payload, digest_size=32).hexdigest() | ||
|
|
||
|
|
||
| _HASHERS: dict[str, Callable[[bytes], str]] = { | ||
| "sha256": _sha256, | ||
| "sha3_256": _sha3_256, | ||
| "blake2b_256": _blake2b_256, | ||
| } | ||
|
|
||
|
|
||
| async def content_checksum_handler(params: dict[str, Any]) -> dict[str, Any]: | ||
| """Hash exact UTF-8 bytes with an allowlisted modern checksum algorithm. | ||
|
|
||
| The input is never Unicode-normalized, so the digest compares the exact | ||
| byte representation Naruon received. Inputs larger than one MiB after | ||
| UTF-8 encoding are rejected before hashing. | ||
|
|
||
| Args: | ||
| params: Validated tool parameters containing ``text`` and ``algorithm``. | ||
|
|
||
| Returns: | ||
| A deterministic checksum receipt with the algorithm, digest, byte | ||
| length, encoding, and an authenticity warning. | ||
|
|
||
| Raises: | ||
| ContentChecksumError: If the algorithm is not allowlisted, the text | ||
| cannot be represented as valid UTF-8, or the encoded content exceeds | ||
| one MiB. Each expected failure carries a stable machine-readable | ||
| ``error_code``. | ||
| """ | ||
| text = params["text"] | ||
| algorithm = params["algorithm"] | ||
| if algorithm not in _HASHERS: | ||
| raise ContentChecksumError( | ||
| "Unsupported checksum algorithm; choose sha256, sha3_256, or blake2b_256", | ||
| error_code="unsupported_checksum_algorithm", | ||
| ) | ||
|
|
||
| try: | ||
| payload = text.encode("utf-8") | ||
| except UnicodeEncodeError as exc: | ||
| raise ContentChecksumError( | ||
| "Content must contain valid Unicode scalar values", | ||
| error_code="content_checksum_invalid_utf8", | ||
| ) from exc | ||
| if len(payload) > MAX_CONTENT_BYTES: | ||
| raise ContentChecksumError( | ||
| f"Content exceeds {MAX_CONTENT_BYTES} UTF-8 bytes", | ||
| error_code="content_checksum_payload_too_large", | ||
| ) | ||
|
|
||
| return { | ||
| "algorithm_code": algorithm, | ||
| "digest_hex": _HASHERS[algorithm](payload), | ||
| "byte_length": len(payload), | ||
| "encoding_code": "utf-8", | ||
| "security_note": SECURITY_NOTE, | ||
| } | ||
|
|
||
|
|
||
| def register_content_checksum_tool() -> None: | ||
| """Register the checksum generator once in Naruon's built-in tool catalog.""" | ||
| if registry.get("content_checksum_generator") is not None: | ||
| return | ||
|
|
||
| registry.register( | ||
| ToolInfo( | ||
| code="content_checksum_generator", | ||
| name="Content checksum generator", | ||
| description=( | ||
| "Compare exact UTF-8 content using SHA-256, SHA-3-256, or " | ||
| "BLAKE2b-256. Choose an algorithm, then compare the returned " | ||
| "digest with the expected value." | ||
| ), | ||
| category="유틸리티", | ||
| parameters={"text": "string", "algorithm": "string"}, | ||
| ), | ||
| content_checksum_handler, | ||
| ) |
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 |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| """Public API contract tests for the bounded content-checksum tool.""" | ||
|
|
||
| import base64 | ||
| import hashlib | ||
| import hmac | ||
| import json | ||
| import os | ||
| import secrets | ||
| import time | ||
|
|
||
| from fastapi.testclient import TestClient | ||
|
|
||
| os.environ.setdefault("AUTH_SESSION_HMAC_SECRET", secrets.token_urlsafe(48)) | ||
|
|
||
| from main import app | ||
|
|
||
|
|
||
| EXPECTED_SHA256_ABC = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" | ||
|
|
||
|
|
||
| def _base64url_encode(raw: bytes) -> str: | ||
| """Encode one JWT segment without padding.""" | ||
| return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") | ||
|
|
||
|
|
||
| def _signed_session_token() -> str: | ||
| """Create a real short-lived HMAC session accepted by the private tools API.""" | ||
| now = int(time.time()) | ||
| header_segment = _base64url_encode( | ||
| json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode( | ||
| "utf-8" | ||
| ) | ||
| ) | ||
| payload_segment = _base64url_encode( | ||
| json.dumps( | ||
| { | ||
| "ver": 1, | ||
| "iss": "naruon-control-plane", | ||
| "aud": "naruon-api", | ||
| "sub": "checksum-contract-user", | ||
| "role": "member", | ||
| "org": "checksum-contract-org", | ||
| "groups": [], | ||
| "workspace": "workspace-checksum-contract-org", | ||
| "iat": now, | ||
| "exp": now + 300, | ||
| }, | ||
| separators=(",", ":"), | ||
| sort_keys=True, | ||
| ).encode("utf-8") | ||
| ) | ||
| signing_input = f"{header_segment}.{payload_segment}" | ||
| signature = hmac.new( | ||
| os.environ["AUTH_SESSION_HMAC_SECRET"].encode("utf-8"), | ||
| signing_input.encode("ascii"), | ||
| hashlib.sha256, | ||
| ).digest() | ||
| return f"{signing_input}.{_base64url_encode(signature)}" | ||
|
|
||
|
|
||
| def _tampered_session_token() -> str: | ||
| """Return a structurally valid session token with a deliberately forged signature.""" | ||
| token = _signed_session_token() | ||
| header_segment, payload_segment, signature_segment = token.split(".") | ||
| signature_padding = "=" * (-len(signature_segment) % 4) | ||
| signature = bytearray( | ||
| base64.urlsafe_b64decode(signature_segment + signature_padding) | ||
| ) | ||
| signature[0] ^= 0x01 | ||
| return f"{header_segment}.{payload_segment}.{_base64url_encode(bytes(signature))}" | ||
|
|
||
|
|
||
| def test_content_checksum_api_executes_authenticated_request() -> None: | ||
| """Startup registration and the authenticated execute route must work together.""" | ||
| with TestClient(app) as client: | ||
| response = client.post( | ||
| "/api/tools/content_checksum_generator/execute", | ||
| headers={"Authorization": f"Bearer {_signed_session_token()}"}, | ||
| json={"parameters": {"text": "abc", "algorithm": "sha256"}}, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| payload = response.json() | ||
| assert payload["status"] == "success" | ||
| assert payload["result"]["digest_hex"] == EXPECTED_SHA256_ABC | ||
| assert payload["result"]["byte_length"] == 3 | ||
| assert payload["message"] == "Execution successful" | ||
|
|
||
|
|
||
| def test_content_checksum_api_rejects_unauthenticated_request() -> None: | ||
| """The checksum execute route must retain the generic tools auth boundary.""" | ||
| with TestClient(app) as client: | ||
| response = client.post( | ||
| "/api/tools/content_checksum_generator/execute", | ||
| json={"parameters": {"text": "abc", "algorithm": "sha256"}}, | ||
| ) | ||
|
|
||
| assert response.status_code == 401 | ||
| assert response.json() == {"detail": "Authentication required"} | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def test_content_checksum_api_rejects_forged_signed_session() -> None: | ||
| """The checksum route must reject a structurally valid token with a forged signature.""" | ||
| with TestClient(app) as client: | ||
| response = client.post( | ||
| "/api/tools/content_checksum_generator/execute", | ||
| headers={"Authorization": f"Bearer {_tampered_session_token()}"}, | ||
| json={"parameters": {"text": "abc", "algorithm": "sha256"}}, | ||
| ) | ||
|
|
||
| assert response.status_code == 401 | ||
| assert response.json() == {"detail": "Authentication required"} | ||
|
|
||
|
|
||
| def test_content_checksum_api_maps_invalid_algorithm_to_execute_failure() -> None: | ||
| """Invalid tool input must expose a stable machine-readable failure code.""" | ||
| with TestClient(app) as client: | ||
| response = client.post( | ||
| "/api/tools/content_checksum_generator/execute", | ||
| headers={"Authorization": f"Bearer {_signed_session_token()}"}, | ||
| json={"parameters": {"text": "abc", "algorithm": "md5"}}, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| payload = response.json() | ||
| assert payload["status"] == "failed" | ||
| assert payload["result"] is None | ||
| assert payload["error_code"] == "unsupported_checksum_algorithm" | ||
| assert "Unsupported checksum algorithm" in payload["message"] | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def test_content_checksum_api_maps_invalid_utf8_to_execute_failure() -> None: | ||
| """Escaped lone surrogates must reach the handler and fail with a stable code.""" | ||
| raw_body = b'{"parameters":{"text":"\\ud800","algorithm":"sha256"}}' | ||
| with TestClient(app) as client: | ||
| response = client.post( | ||
| "/api/tools/content_checksum_generator/execute", | ||
| headers={ | ||
| "Authorization": f"Bearer {_signed_session_token()}", | ||
| "Content-Type": "application/json", | ||
| }, | ||
| content=raw_body, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| payload = response.json() | ||
| assert payload["status"] == "failed" | ||
| assert payload["result"] is None | ||
| assert payload["error_code"] == "content_checksum_invalid_utf8" | ||
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.
Uh oh!
There was an error while loading. Please reload this page.