From ca3ce5f9b0323e8136478e1b76dab4e30b2707bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:48:14 +0900 Subject: [PATCH 01/48] test(tools): define secure content checksum contract --- backend/tests/test_content_checksum_tool.py | 106 ++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 backend/tests/test_content_checksum_tool.py diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py new file mode 100644 index 000000000..d96d89dab --- /dev/null +++ b/backend/tests/test_content_checksum_tool.py @@ -0,0 +1,106 @@ +"""Regression tests for Naruon's bounded content-checksum tool.""" + +import pytest + +import main # noqa: F401 # Importing the application registers built-in tools. +from api.tools import registry + + +SECURITY_NOTE = ( + "Use this digest to compare exact content bytes; it does not authenticate " + "the sender or replace a MAC/signature." +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("algorithm", "expected_digest"), + [ + ( + "sha256", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + ), + ( + "sha3_256", + "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + ), + ( + "blake2b_256", + "bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319", + ), + ], +) +async def test_content_checksum_generator_matches_published_vectors( + algorithm: str, + expected_digest: str, +) -> None: + """The catalog tool must return stable standards-based digests for exact bytes.""" + tool = registry.get("content_checksum_generator") + + assert tool is not None + assert tool.parameters == {"text": "string", "algorithm": "string"} + + result = await registry.invoke_tool( + "content_checksum_generator", + {"text": "abc", "algorithm": algorithm}, + ) + + assert result == { + "algorithm_code": algorithm, + "digest_hex": expected_digest, + "byte_length": 3, + "encoding_code": "utf-8", + "security_note": SECURITY_NOTE, + } + + +@pytest.mark.asyncio +async def test_content_checksum_generator_hashes_exact_utf8_without_normalizing() -> None: + """Canonically equivalent Unicode strings must remain distinct exact-byte inputs.""" + composed = await registry.invoke_tool( + "content_checksum_generator", + {"text": "é", "algorithm": "sha256"}, + ) + decomposed = await registry.invoke_tool( + "content_checksum_generator", + {"text": "e\u0301", "algorithm": "sha256"}, + ) + + assert composed["byte_length"] == 2 + assert composed["digest_hex"] == ( + "4a99557e4033c3539de2eb65472017cad5f9557f7a0625a09f1c3f6e2ba69c4c" + ) + assert decomposed["byte_length"] == 3 + assert decomposed["digest_hex"] == ( + "bf12767b0f2a56b2190075bae8169f656e3ce8d6357d4aff184bc6c7ea48f9f6" + ) + assert composed["digest_hex"] != decomposed["digest_hex"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("algorithm", ["sha1", "md5", "SHA256", "sha-256", ""]) +async def test_content_checksum_generator_rejects_unapproved_algorithm_names( + algorithm: str, +) -> None: + """Legacy or ambiguous algorithm names must fail closed instead of being guessed.""" + with pytest.raises(ValueError, match="Unsupported checksum algorithm"): + await registry.invoke_tool( + "content_checksum_generator", + {"text": "abc", "algorithm": algorithm}, + ) + + +@pytest.mark.asyncio +async def test_content_checksum_generator_enforces_utf8_byte_limit() -> None: + """The one-mebibyte boundary is measured after UTF-8 encoding.""" + accepted = await registry.invoke_tool( + "content_checksum_generator", + {"text": "a" * 1_048_576, "algorithm": "sha256"}, + ) + assert accepted["byte_length"] == 1_048_576 + + with pytest.raises(ValueError, match="Content exceeds 1048576 UTF-8 bytes"): + await registry.invoke_tool( + "content_checksum_generator", + {"text": "é" * 524_289, "algorithm": "sha256"}, + ) From 4c794add87db71904f303d995b87012a9dfa826a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:50:18 +0900 Subject: [PATCH 02/48] feat(tools): implement bounded content checksum generator --- backend/api/content_checksum_tool.py | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 backend/api/content_checksum_tool.py diff --git a/backend/api/content_checksum_tool.py b/backend/api/content_checksum_tool.py new file mode 100644 index 000000000..d3da0134e --- /dev/null +++ b/backend/api/content_checksum_tool.py @@ -0,0 +1,101 @@ +"""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." +) + + +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: + ValueError: If the algorithm is not allowlisted or the encoded content + exceeds one MiB. + """ + text = params["text"] + algorithm = params["algorithm"] + if algorithm not in _HASHERS: + raise ValueError( + "Unsupported checksum algorithm; choose sha256, sha3_256, or blake2b_256" + ) + + payload = text.encode("utf-8") + if len(payload) > MAX_CONTENT_BYTES: + raise ValueError(f"Content exceeds {MAX_CONTENT_BYTES} UTF-8 bytes") + + 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, + ) From cf293ea671ef942fdc431a8f4cd3ba597d250cb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:50:54 +0900 Subject: [PATCH 03/48] feat(tools): register content checksum generator --- backend/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/main.py b/backend/main.py index 0ad7762a8..8e79752de 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,6 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from api.auth import get_auth_context, preload_oidc_jwks +from api.content_checksum_tool import register_content_checksum_tool from api.search import router as search_router from api.llm import router as llm_router from api.calendar import router as calendar_router @@ -41,6 +42,8 @@ from services.reply_sla_scheduler import ReplySlaScheduler from prometheus_fastapi_instrumentator import Instrumentator +register_content_checksum_tool() + imap_worker = ImapSyncWorker() pop3_worker = Pop3SyncWorker() reply_sla_scheduler = ReplySlaScheduler() From a05de815e6fe1c7b85ecb646b270ecc169f86ee5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:51:28 +0900 Subject: [PATCH 04/48] test(tools): cover checksum registration lifecycle --- backend/tests/test_content_checksum_tool.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index d96d89dab..09127aa00 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -3,6 +3,7 @@ import pytest import main # noqa: F401 # Importing the application registers built-in tools. +from api.content_checksum_tool import register_content_checksum_tool from api.tools import registry @@ -12,6 +13,16 @@ ) +def test_content_checksum_registration_is_idempotent() -> None: + """Repeated startup registration must preserve the existing catalog entry.""" + original = registry.get("content_checksum_generator") + + assert original is not None + register_content_checksum_tool() + + assert registry.get("content_checksum_generator") is original + + @pytest.mark.asyncio @pytest.mark.parametrize( ("algorithm", "expected_digest"), From d0e5639b329e50d667e21ccc12ed0dea692e0100 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:52:03 +0900 Subject: [PATCH 05/48] docs(doctoring): record checksum standards and scope --- docs/doctoring/content-checksum-generator.md | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/doctoring/content-checksum-generator.md diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md new file mode 100644 index 000000000..13a8bb4e7 --- /dev/null +++ b/docs/doctoring/content-checksum-generator.md @@ -0,0 +1,37 @@ +# Content checksum generator — standards and product boundary + +**Status:** active PR candidate; not shipped until merged through protected `develop`. + +**Traceability:** bounded checksum slice of issue #1247, *Build an auditable data-hygiene utility suite*. + +## Product contract + +Naruon's `content_checksum_generator` compares the exact UTF-8 byte sequence supplied by the caller. It deliberately does **not** Unicode-normalize input before hashing, because normalization would change the byte-level evidence being compared. The deterministic tool accepts at most 1,048,576 UTF-8 bytes per invocation and exposes only these normal-surface algorithms: + +- `sha256` — SHA-256 from FIPS 180-4; +- `sha3_256` — SHA3-256 from FIPS 202; +- `blake2b_256` — BLAKE2b with a 256-bit digest, using the BLAKE2 construction standardized in RFC 7693. + +MD5 and SHA-1 are intentionally absent from the normal surface. NIST states that SHA-1 is being transitioned out for applying cryptographic protection by December 31, 2030; Naruon therefore does not introduce SHA-1 as a new customer-facing checksum choice. No legacy-compatibility checksum mode is part of this slice. + +A returned digest is an equality/integrity fingerprint for the exact supplied bytes. It does not authenticate a sender, prove provenance, or replace a keyed MAC or digital signature. Customer-facing output carries that warning with every result so the next action is explicit: compare the digest with an independently obtained expected digest when checking content equality; use an authenticated construction when sender or origin authenticity matters. + +## Standards status reviewed 2026-08-15 + +FIPS 180-4 remains NIST's final Secure Hash Standard publication while NIST has announced a future revision. FIPS 202 remains NIST's final SHA-3 standard while NIST has announced an update process. These planning notes are not treated as replacement standards before a successor is finalized. RFC 7693 is the RFC Editor publication describing BLAKE2. + +The implementation uses Python's standard-library `hashlib` bindings only; this slice adds no external cryptographic dependency and no model-mediated decision path. Deterministic checksum behavior therefore remains independent of LLM judgment and credentials. + +## Acceptance evidence + +The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, coverage, and independent review gates before the capability may be described as shipped. + +## References (APA 7th) + +National Institute of Standards and Technology. (2015). *Secure hash standard (SHS)* (FIPS PUB 180-4). U.S. Department of Commerce. https://doi.org/10.6028/NIST.FIPS.180-4 + +National Institute of Standards and Technology. (2015). *SHA-3 standard: Permutation-based hash and extendable-output functions* (FIPS PUB 202). U.S. Department of Commerce. https://doi.org/10.6028/NIST.FIPS.202 + +National Institute of Standards and Technology. (2022, December 15). *NIST transitioning away from SHA-1 for all applications* (updated February 3, 2025). https://www.nist.gov/news-events/news/2022/12/nist-transitioning-away-sha-1-all-applications + +Saarinen, M.-J. O., & Aumasson, J.-P. (2015). *The BLAKE2 cryptographic hash and message authentication code (MAC)* (RFC 7693). RFC Editor. https://doi.org/10.17487/RFC7693 From e1b559e0c07b554484e1f6cce128148ea889cde5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:53:05 +0900 Subject: [PATCH 06/48] docs(adr): bound customer checksum algorithm surface --- .../0004-bounded-content-checksum-surface.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/adr/0004-bounded-content-checksum-surface.md diff --git a/docs/adr/0004-bounded-content-checksum-surface.md b/docs/adr/0004-bounded-content-checksum-surface.md new file mode 100644 index 000000000..b33a065b0 --- /dev/null +++ b/docs/adr/0004-bounded-content-checksum-surface.md @@ -0,0 +1,49 @@ +# ADR-0004: Bound the customer content-checksum algorithm surface + +**Status:** Accepted + +**Date:** 2026-08-15 + +**Decision owner:** Naruon maintainers + +**Capability maturity:** deterministic tool contract; runtime availability remains subject to protected-branch integration and verification + +**Scope:** Naruon's customer-facing deterministic content-checksum utility only. This ADR does not define sender authentication, signatures, password hashing, key derivation, or external artifact-signing policy. + +**Related issue:** #1247 + +## Context + +A checksum utility is useful for comparing exported email/document text, audit evidence, and workflow payloads, but an unconstrained generic hash selector creates two avoidable product risks. First, legacy algorithms such as MD5 or SHA-1 can be misread as recommended security controls. Second, digest output can be mistaken for proof of who produced a value even though an unkeyed hash authenticates neither sender nor origin. + +Naruon's deterministic tool registry already supplies a stable execution boundary. The smallest defensible slice is therefore an explicit modern algorithm allowlist with bounded exact-byte input and a machine-visible authenticity warning, rather than a wrapper over arbitrary `hashlib` names. + +## Decision + +1. The normal customer surface accepts exactly `sha256`, `sha3_256`, and `blake2b_256`. +2. MD5, SHA-1, aliases, case variants, and unknown names fail closed; no compatibility guessing is performed. +3. Text is encoded as UTF-8 exactly as supplied and is not Unicode-normalized before hashing. +4. One invocation accepts at most 1,048,576 encoded bytes so the generic tool endpoint cannot become an unbounded hashing sink. +5. The result records the selected algorithm, hexadecimal digest, encoded byte length, encoding, and an explicit warning that the digest does not authenticate a sender or replace a MAC/signature. +6. The implementation uses the Python standard library and remains deterministic and independent of model judgment or LLM credentials. +7. A future legacy compatibility mode requires a separate reviewed decision with an explicit non-security acknowledgement; this ADR does not authorize one. + +## Consequences + +- Buyers can compare exact content evidence without being steered toward a legacy digest. +- Canonically equivalent Unicode strings may intentionally produce different digests when their UTF-8 byte sequences differ; this is correct for exact-byte evidence. +- Callers that need origin authenticity must select an authenticated construction outside this tool. +- Algorithm expansion is a product/security decision rather than a free-form runtime option and requires tests plus standards review. + +## Verification + +The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. + +Standards status and APA 7 references are maintained in [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). + +## Alternatives rejected + +- **Expose every `hashlib` algorithm:** transfers a cryptographic policy decision to callers and makes legacy options look supported. +- **Default to SHA-1 or MD5 for interoperability:** creates a new normal-surface dependency on algorithms Naruon should not recommend for security-labelled use. +- **Normalize Unicode before hashing:** destroys the exact byte-level comparison contract and can make different source evidence converge silently. +- **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. From a514f314b2ad0cfddbc42cba17704f3c732d49c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:53:25 +0900 Subject: [PATCH 07/48] docs(adr): index checksum surface decision --- docs/adr/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/adr/README.md b/docs/adr/README.md index 097ff8598..44e29d181 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -12,6 +12,7 @@ govern implementation. | [ADR-0001](0001-topic-measurement-authority.md) | Naruon-local policy for consuming structural topic measurement, never a keyword/label heuristic | Accepted | `ACCEPTED-NARUON-POLICY`; no runtime promotion | | [ADR-0002](0002-fitted-topic-artifact-consumption.md) | Conditionally consume only a versioned fitted topic artifact through a fail-closed adapter | Proposed | Target `PLANNED`; runtime `BLOCKED-UPSTREAM` | | [ADR-0003](0003-separate-topic-measurement-from-agenda-generation.md) | Keep statistical measurement separate from agenda generation | Proposed | Target and future capability `PLANNED`; no implementation authorization | +| [ADR-0004](0004-bounded-content-checksum-surface.md) | Bound the customer checksum surface to modern allowlisted algorithms and exact UTF-8 evidence | Accepted | Deterministic tool contract; runtime remains protected-integration gated | The complete topic-intelligence requirements, architecture, contract, UML, conceptual ERD, security, test, and operability graph is indexed at From 5e5448a1d7bc26410d982894f9dbf144a6f64239 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:53:55 +0900 Subject: [PATCH 08/48] docs(operations): document checksum customer workflow --- docs/operations/content-checksum-generator.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/operations/content-checksum-generator.md diff --git a/docs/operations/content-checksum-generator.md b/docs/operations/content-checksum-generator.md new file mode 100644 index 000000000..359684160 --- /dev/null +++ b/docs/operations/content-checksum-generator.md @@ -0,0 +1,40 @@ +# Content checksum generator + +**Availability:** this capability is not shipped until its implementation passes protected-`develop` integration and review gates. + +Use `content_checksum_generator` when a customer needs to determine whether two pieces of text have the same exact UTF-8 byte representation. It is appropriate for export verification, evidence comparison, and reproducible workflow receipts. It is not sender authentication and it does not replace a MAC or digital signature. + +## Decide which algorithm to use + +- Choose `sha256` for the broadest SHA-2 interoperability. +- Choose `sha3_256` when the receiving workflow explicitly uses SHA3-256. +- Choose `blake2b_256` when both sides agree on BLAKE2b with a 256-bit digest. + +Do not substitute MD5, SHA-1, aliases, or differently sized BLAKE2 outputs; the tool rejects them rather than guessing what the caller intended. + +## Execute + +Call the existing authenticated tool endpoint with the tool code and the two required parameters: + +```json +{ + "text": "content to compare", + "algorithm": "sha256" +} +``` + +The tool reports `algorithm_code`, `digest_hex`, `byte_length`, `encoding_code`, and `security_note`. Input is limited to 1,048,576 bytes **after** UTF-8 encoding. Canonically equivalent Unicode text can produce different digests when its byte sequences differ because Naruon does not normalize the source before hashing. + +## Take the next action + +1. Obtain the expected digest through an independent trusted channel or from the system that produced the reference artifact. +2. Confirm the algorithm identifiers are identical on both sides. +3. Compare the complete hexadecimal digests exactly. +4. If they differ, treat the contents as non-identical and investigate source encoding/content provenance; do not truncate or approximately compare the digest. +5. If the business decision depends on who created or authorized the content, use the relevant authenticated provenance/signature workflow instead of interpreting checksum equality as identity proof. + +## Failure handling + +An unsupported algorithm or oversized payload fails closed with a deterministic validation error. Operators should change the requested algorithm to an allowlisted value or split/restructure the calling workflow; they should not bypass the limit or add a legacy digest solely to make a failed request pass. + +Architecture decision: [`ADR-0004`](../adr/0004-bounded-content-checksum-surface.md). Standards and APA 7 references: [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). From b3d0bc58930c1843caae21f7b9667848138267f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:57:44 +0900 Subject: [PATCH 09/48] docs(adr): avoid concurrent ADR numbering collision --- .../0007-bounded-content-checksum-surface.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/adr/0007-bounded-content-checksum-surface.md diff --git a/docs/adr/0007-bounded-content-checksum-surface.md b/docs/adr/0007-bounded-content-checksum-surface.md new file mode 100644 index 000000000..e307fbd4e --- /dev/null +++ b/docs/adr/0007-bounded-content-checksum-surface.md @@ -0,0 +1,49 @@ +# ADR-0007: Bound the customer content-checksum algorithm surface + +**Status:** Accepted + +**Date:** 2026-08-15 + +**Decision owner:** Naruon maintainers + +**Capability maturity:** deterministic tool contract; runtime availability remains subject to protected-branch integration and verification + +**Scope:** Naruon's customer-facing deterministic content-checksum utility only. This ADR does not define sender authentication, signatures, password hashing, key derivation, or external artifact-signing policy. + +**Related issue:** #1247 + +## Context + +A checksum utility is useful for comparing exported email/document text, audit evidence, and workflow payloads, but an unconstrained generic hash selector creates two avoidable product risks. First, legacy algorithms such as MD5 or SHA-1 can be misread as recommended security controls. Second, digest output can be mistaken for proof of who produced a value even though an unkeyed hash authenticates neither sender nor origin. + +Naruon's deterministic tool registry already supplies a stable execution boundary. The smallest defensible slice is therefore an explicit modern algorithm allowlist with bounded exact-byte input and a machine-visible authenticity warning, rather than a wrapper over arbitrary `hashlib` names. + +## Decision + +1. The normal customer surface accepts exactly `sha256`, `sha3_256`, and `blake2b_256`. +2. MD5, SHA-1, aliases, case variants, and unknown names fail closed; no compatibility guessing is performed. +3. Text is encoded as UTF-8 exactly as supplied and is not Unicode-normalized before hashing. +4. One invocation accepts at most 1,048,576 encoded bytes so the generic tool endpoint cannot become an unbounded hashing sink. +5. The result records the selected algorithm, hexadecimal digest, encoded byte length, encoding, and an explicit warning that the digest does not authenticate a sender or replace a MAC/signature. +6. The implementation uses the Python standard library and remains deterministic and independent of model judgment or LLM credentials. +7. A future legacy compatibility mode requires a separate reviewed decision with an explicit non-security acknowledgement; this ADR does not authorize one. + +## Consequences + +- Buyers can compare exact content evidence without being steered toward a legacy digest. +- Canonically equivalent Unicode strings may intentionally produce different digests when their UTF-8 byte sequences differ; this is correct for exact-byte evidence. +- Callers that need origin authenticity must select an authenticated construction outside this tool. +- Algorithm expansion is a product/security decision rather than a free-form runtime option and requires tests plus standards review. + +## Verification + +The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. + +Standards status and APA 7 references are maintained in [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). + +## Alternatives rejected + +- **Expose every `hashlib` algorithm:** transfers a cryptographic policy decision to callers and makes legacy options look supported. +- **Default to SHA-1 or MD5 for interoperability:** creates a new normal-surface dependency on algorithms Naruon should not recommend for security-labelled use. +- **Normalize Unicode before hashing:** destroys the exact byte-level comparison contract and can make different source evidence converge silently. +- **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. From c3057cd9bd6f6e507e288e3a9359892210e96991 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:58:22 +0900 Subject: [PATCH 10/48] docs(adr): reserve checksum decision as ADR-0007 --- docs/adr/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 44e29d181..130585e19 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -12,7 +12,7 @@ govern implementation. | [ADR-0001](0001-topic-measurement-authority.md) | Naruon-local policy for consuming structural topic measurement, never a keyword/label heuristic | Accepted | `ACCEPTED-NARUON-POLICY`; no runtime promotion | | [ADR-0002](0002-fitted-topic-artifact-consumption.md) | Conditionally consume only a versioned fitted topic artifact through a fail-closed adapter | Proposed | Target `PLANNED`; runtime `BLOCKED-UPSTREAM` | | [ADR-0003](0003-separate-topic-measurement-from-agenda-generation.md) | Keep statistical measurement separate from agenda generation | Proposed | Target and future capability `PLANNED`; no implementation authorization | -| [ADR-0004](0004-bounded-content-checksum-surface.md) | Bound the customer checksum surface to modern allowlisted algorithms and exact UTF-8 evidence | Accepted | Deterministic tool contract; runtime remains protected-integration gated | +| [ADR-0007](0007-bounded-content-checksum-surface.md) | Bound the customer checksum surface to modern allowlisted algorithms and exact UTF-8 evidence | Accepted | Deterministic tool contract; runtime remains protected-integration gated | The complete topic-intelligence requirements, architecture, contract, UML, conceptual ERD, security, test, and operability graph is indexed at From 2898533a8d7264b2416cb9c25ea2719d78baab5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 23:58:59 +0900 Subject: [PATCH 11/48] docs(operations): point checksum workflow to ADR-0007 --- docs/operations/content-checksum-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/content-checksum-generator.md b/docs/operations/content-checksum-generator.md index 359684160..7ff823502 100644 --- a/docs/operations/content-checksum-generator.md +++ b/docs/operations/content-checksum-generator.md @@ -37,4 +37,4 @@ The tool reports `algorithm_code`, `digest_hex`, `byte_length`, `encoding_code`, An unsupported algorithm or oversized payload fails closed with a deterministic validation error. Operators should change the requested algorithm to an allowlisted value or split/restructure the calling workflow; they should not bypass the limit or add a legacy digest solely to make a failed request pass. -Architecture decision: [`ADR-0004`](../adr/0004-bounded-content-checksum-surface.md). Standards and APA 7 references: [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). +Architecture decision: [`ADR-0007`](../adr/0007-bounded-content-checksum-surface.md). Standards and APA 7 references: [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). From d5245e945e18393d0dccf6619fbf3f465696d5c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 00:00:55 +0900 Subject: [PATCH 12/48] docs(adr): remove conflicting ADR-0004 path --- .../0004-bounded-content-checksum-surface.md | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 docs/adr/0004-bounded-content-checksum-surface.md diff --git a/docs/adr/0004-bounded-content-checksum-surface.md b/docs/adr/0004-bounded-content-checksum-surface.md deleted file mode 100644 index b33a065b0..000000000 --- a/docs/adr/0004-bounded-content-checksum-surface.md +++ /dev/null @@ -1,49 +0,0 @@ -# ADR-0004: Bound the customer content-checksum algorithm surface - -**Status:** Accepted - -**Date:** 2026-08-15 - -**Decision owner:** Naruon maintainers - -**Capability maturity:** deterministic tool contract; runtime availability remains subject to protected-branch integration and verification - -**Scope:** Naruon's customer-facing deterministic content-checksum utility only. This ADR does not define sender authentication, signatures, password hashing, key derivation, or external artifact-signing policy. - -**Related issue:** #1247 - -## Context - -A checksum utility is useful for comparing exported email/document text, audit evidence, and workflow payloads, but an unconstrained generic hash selector creates two avoidable product risks. First, legacy algorithms such as MD5 or SHA-1 can be misread as recommended security controls. Second, digest output can be mistaken for proof of who produced a value even though an unkeyed hash authenticates neither sender nor origin. - -Naruon's deterministic tool registry already supplies a stable execution boundary. The smallest defensible slice is therefore an explicit modern algorithm allowlist with bounded exact-byte input and a machine-visible authenticity warning, rather than a wrapper over arbitrary `hashlib` names. - -## Decision - -1. The normal customer surface accepts exactly `sha256`, `sha3_256`, and `blake2b_256`. -2. MD5, SHA-1, aliases, case variants, and unknown names fail closed; no compatibility guessing is performed. -3. Text is encoded as UTF-8 exactly as supplied and is not Unicode-normalized before hashing. -4. One invocation accepts at most 1,048,576 encoded bytes so the generic tool endpoint cannot become an unbounded hashing sink. -5. The result records the selected algorithm, hexadecimal digest, encoded byte length, encoding, and an explicit warning that the digest does not authenticate a sender or replace a MAC/signature. -6. The implementation uses the Python standard library and remains deterministic and independent of model judgment or LLM credentials. -7. A future legacy compatibility mode requires a separate reviewed decision with an explicit non-security acknowledgement; this ADR does not authorize one. - -## Consequences - -- Buyers can compare exact content evidence without being steered toward a legacy digest. -- Canonically equivalent Unicode strings may intentionally produce different digests when their UTF-8 byte sequences differ; this is correct for exact-byte evidence. -- Callers that need origin authenticity must select an authenticated construction outside this tool. -- Algorithm expansion is a product/security decision rather than a free-form runtime option and requires tests plus standards review. - -## Verification - -The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. - -Standards status and APA 7 references are maintained in [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). - -## Alternatives rejected - -- **Expose every `hashlib` algorithm:** transfers a cryptographic policy decision to callers and makes legacy options look supported. -- **Default to SHA-1 or MD5 for interoperability:** creates a new normal-surface dependency on algorithms Naruon should not recommend for security-labelled use. -- **Normalize Unicode before hashing:** destroys the exact byte-level comparison contract and can make different source evidence converge silently. -- **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. From 1371f9ac2dc49b6b3526ad2fcc0e3f84fc0d7926 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 00:01:46 +0900 Subject: [PATCH 13/48] docs(doctoring): ground checksum choices in current standards and research --- docs/doctoring/content-checksum-generator.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md index 13a8bb4e7..8a3977cae 100644 --- a/docs/doctoring/content-checksum-generator.md +++ b/docs/doctoring/content-checksum-generator.md @@ -18,20 +18,37 @@ A returned digest is an equality/integrity fingerprint for the exact supplied by ## Standards status reviewed 2026-08-15 -FIPS 180-4 remains NIST's final Secure Hash Standard publication while NIST has announced a future revision. FIPS 202 remains NIST's final SHA-3 standard while NIST has announced an update process. These planning notes are not treated as replacement standards before a successor is finalized. RFC 7693 is the RFC Editor publication describing BLAKE2. +The official NIST publication page still lists FIPS 180-4 (2015) as the final Secure Hash Standard. NIST's March 7, 2023 Crypto Publication Review Board decision says FIPS 180-4 will be revised, including removal of the SHA-1 specification, but that decision is a revision plan rather than a replacement final standard. The official NIST publication page still lists FIPS 202 (2015) as the final SHA-3 standard. NIST's March 12, 2025 decision says FIPS 202 will be updated and SP 800-185 revised, with the normal draft/public-comment process to follow. No successor final publication was identified in the official status pages reviewed on 2026-08-15, so Naruon continues to cite the existing final standards while separately recording the announced updates. RFC 7693 remains the RFC Editor publication describing BLAKE2. The implementation uses Python's standard-library `hashlib` bindings only; this slice adds no external cryptographic dependency and no model-mediated decision path. Deterministic checksum behavior therefore remains independent of LLM judgment and credentials. +## Research grounding + +Two primary peer-reviewed cryptography papers are directly relevant to the non-SHA-2 choices in this bounded surface: + +- Bertoni, Daemen, Peeters, and Van Assche (2008) prove the indifferentiability properties of the sponge construction that underpins Keccak/SHA-3. That work supports treating SHA-3 as a distinct, standardized sponge-based hash construction rather than an alias for SHA-2. +- Aumasson, Neves, Wilcox-O'Hearn, and Winnerlein (2013) introduce BLAKE2 and describe BLAKE2b as the 64-bit-oriented variant, including its software-performance and security design goals. That primary design paper is the research basis for exposing BLAKE2b only under an explicit 256-bit output identifier rather than as an ambiguous generic `blake2` option. + +No paper PDF is committed in this slice because redistribution permission for the publisher versions was not established from the primary publication records during this review. The citations and DOI links below are therefore the auditable research traceability; this avoids assuming redistribution rights merely because a paper can be viewed online. + ## Acceptance evidence The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, coverage, and independent review gates before the capability may be described as shipped. ## References (APA 7th) +Aumasson, J.-P., Neves, S., Wilcox-O'Hearn, Z., & Winnerlein, C. (2013). BLAKE2: Simpler, smaller, fast as MD5. In *Applied cryptography and network security* (Lecture Notes in Computer Science, Vol. 7954, pp. 119–135). Springer. https://doi.org/10.1007/978-3-642-38980-1_8 + +Bertoni, G., Daemen, J., Peeters, M., & Van Assche, G. (2008). On the indifferentiability of the sponge construction. In *Advances in cryptology – EUROCRYPT 2008* (Lecture Notes in Computer Science, Vol. 4965, pp. 181–197). Springer. https://doi.org/10.1007/978-3-540-78967-3_11 + National Institute of Standards and Technology. (2015). *Secure hash standard (SHS)* (FIPS PUB 180-4). U.S. Department of Commerce. https://doi.org/10.6028/NIST.FIPS.180-4 National Institute of Standards and Technology. (2015). *SHA-3 standard: Permutation-based hash and extendable-output functions* (FIPS PUB 202). U.S. Department of Commerce. https://doi.org/10.6028/NIST.FIPS.202 National Institute of Standards and Technology. (2022, December 15). *NIST transitioning away from SHA-1 for all applications* (updated February 3, 2025). https://www.nist.gov/news-events/news/2022/12/nist-transitioning-away-sha-1-all-applications +National Institute of Standards and Technology. (2023, March 7). *Decision to revise FIPS 180-4, Secure Hash Standard (SHS)* (updated February 3, 2025). https://www.nist.gov/news-events/news/2023/03/decision-revise-fips-180-4-secure-hash-standard-shs + +National Institute of Standards and Technology. (2025, March 12). *Decision to update FIPS 202 and revise SP 800-185*. Computer Security Resource Center. https://csrc.nist.gov/News/2025/decision-to-update-fips-202-and-revise-sp-800-185 + Saarinen, M.-J. O., & Aumasson, J.-P. (2015). *The BLAKE2 cryptographic hash and message authentication code (MAC)* (RFC 7693). RFC Editor. https://doi.org/10.17487/RFC7693 From 3e6f7914b1efe5e504848388e6690102eb315376 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 00:02:29 +0900 Subject: [PATCH 14/48] docs(operations): specify authenticated checksum endpoint contract --- docs/operations/content-checksum-generator.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/operations/content-checksum-generator.md b/docs/operations/content-checksum-generator.md index 7ff823502..1b4c6ef29 100644 --- a/docs/operations/content-checksum-generator.md +++ b/docs/operations/content-checksum-generator.md @@ -14,16 +14,23 @@ Do not substitute MD5, SHA-1, aliases, or differently sized BLAKE2 outputs; the ## Execute -Call the existing authenticated tool endpoint with the tool code and the two required parameters: +Use the canonical generic tool-execution contract implemented by [`backend/api/tools.py`](../../backend/api/tools.py): + +- **Method and route:** `POST /api/tools/content_checksum_generator/execute`. +- **Authentication prerequisite:** send `Authorization: Bearer ` with a bearer token accepted by Naruon's [`get_auth_context`](../../backend/api/auth.py). The tools router is mounted with that private-API dependency in [`backend/main.py`](../../backend/main.py); unauthenticated requests are not part of the supported contract. +- **Content type:** `application/json`. +- **Request envelope:** place tool inputs under the required `parameters` object; do not send `text` or `algorithm` at the top level. ```json { - "text": "content to compare", - "algorithm": "sha256" + "parameters": { + "text": "content to compare", + "algorithm": "sha256" + } } ``` -The tool reports `algorithm_code`, `digest_hex`, `byte_length`, `encoding_code`, and `security_note`. Input is limited to 1,048,576 bytes **after** UTF-8 encoding. Canonically equivalent Unicode text can produce different digests when its byte sequences differ because Naruon does not normalize the source before hashing. +On successful execution, the endpoint returns the generic `ExecuteResponse` envelope with `status: "success"`; its `result` contains `algorithm_code`, `digest_hex`, `byte_length`, `encoding_code`, and `security_note`. Input is limited to 1,048,576 bytes **after** UTF-8 encoding. Canonically equivalent Unicode text can produce different digests when its byte sequences differ because Naruon does not normalize the source before hashing. ## Take the next action @@ -35,6 +42,6 @@ The tool reports `algorithm_code`, `digest_hex`, `byte_length`, `encoding_code`, ## Failure handling -An unsupported algorithm or oversized payload fails closed with a deterministic validation error. Operators should change the requested algorithm to an allowlisted value or split/restructure the calling workflow; they should not bypass the limit or add a legacy digest solely to make a failed request pass. +An unsupported algorithm or oversized payload fails closed with a deterministic `ExecuteResponse` whose `status` is `"failed"` and whose message is a bounded validation error. Operators should change the requested algorithm to an allowlisted value or split/restructure the calling workflow; they should not bypass the limit or add a legacy digest solely to make a failed request pass. Architecture decision: [`ADR-0007`](../adr/0007-bounded-content-checksum-surface.md). Standards and APA 7 references: [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). From fce902fa6bc3157d023ed1140dae1349251d1f79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 01:04:49 +0900 Subject: [PATCH 15/48] test(tools): use application bootstrap import explicitly --- backend/tests/test_content_checksum_tool.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 09127aa00..5388b19ae 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -2,7 +2,7 @@ import pytest -import main # noqa: F401 # Importing the application registers built-in tools. +import main from api.content_checksum_tool import register_content_checksum_tool from api.tools import registry @@ -13,6 +13,12 @@ ) +def test_application_bootstrap_registers_content_checksum_tool() -> None: + """Loading the FastAPI application must expose the built-in checksum tool.""" + assert main.app is not None + assert registry.get("content_checksum_generator") is not None + + def test_content_checksum_registration_is_idempotent() -> None: """Repeated startup registration must preserve the existing catalog entry.""" original = registry.get("content_checksum_generator") From 5a39a0766063d05719960e9a9b152f333ca57240 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 01:10:56 +0900 Subject: [PATCH 16/48] docs(checksum): make coverage acceptance explicit --- docs/doctoring/content-checksum-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md index 8a3977cae..9cc301428 100644 --- a/docs/doctoring/content-checksum-generator.md +++ b/docs/doctoring/content-checksum-generator.md @@ -33,7 +33,7 @@ No paper PDF is committed in this slice because redistribution permission for th ## Acceptance evidence -The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, coverage, and independent review gates before the capability may be described as shipped. +The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. ## References (APA 7th) From fdec2e46ba2195627e254dd611957902ba3b1ae4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 05:53:59 +0900 Subject: [PATCH 17/48] test(tools): cover checksum API contract --- backend/tests/test_content_checksum_api.py | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 backend/tests/test_content_checksum_api.py diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py new file mode 100644 index 000000000..8e07af4f0 --- /dev/null +++ b/backend/tests/test_content_checksum_api.py @@ -0,0 +1,105 @@ +"""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 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"] == "Tool executed successfully" + + +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"} + + +def test_content_checksum_api_maps_invalid_algorithm_to_execute_failure() -> None: + """Invalid tool input must use the generic ExecuteResponse failure contract.""" + 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 "Unsupported checksum algorithm" in payload["message"] From 5816a82231a5131337faf449b9f367449b44b53a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 05:55:12 +0900 Subject: [PATCH 18/48] test(tools): align checksum API success contract --- backend/tests/test_content_checksum_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py index 8e07af4f0..79bf5df83 100644 --- a/backend/tests/test_content_checksum_api.py +++ b/backend/tests/test_content_checksum_api.py @@ -74,7 +74,7 @@ def test_content_checksum_api_executes_authenticated_request() -> None: assert payload["status"] == "success" assert payload["result"]["digest_hex"] == EXPECTED_SHA256_ABC assert payload["result"]["byte_length"] == 3 - assert payload["message"] == "Tool executed successfully" + assert payload["message"] == "Execution successful" def test_content_checksum_api_rejects_unauthenticated_request() -> None: From 08698f402704a8a45a513e57aaf4793e44bab225 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 06:53:42 +0900 Subject: [PATCH 19/48] test(tools): exercise checksum API auth contract --- .../test_content_checksum_api_contract.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 backend/tests/test_content_checksum_api_contract.py diff --git a/backend/tests/test_content_checksum_api_contract.py b/backend/tests/test_content_checksum_api_contract.py new file mode 100644 index 000000000..99977bafb --- /dev/null +++ b/backend/tests/test_content_checksum_api_contract.py @@ -0,0 +1,105 @@ +"""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 JWT bytes using unpadded base64url.""" + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + + +def _signed_session_token() -> str: + """Build a short-lived session token accepted by the real auth dependency.""" + 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": "org-acme", + "groups": ["group-1"], + "workspace": "workspace-org-acme", + "exp": int(time.time()) + 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 test_checksum_execute_route_requires_authenticated_session() -> None: + """The private checksum endpoint must reject requests without auth.""" + 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"} + + +def test_checksum_execute_route_returns_authenticated_digest() -> None: + """Startup must expose the checksum tool through the documented envelope.""" + 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["message"] is None + assert payload["result"]["algorithm_code"] == "sha256" + assert payload["result"]["digest_hex"] == EXPECTED_SHA256_ABC + assert payload["result"]["byte_length"] == 3 + assert payload["result"]["encoding_code"] == "utf-8" + + +def test_checksum_execute_route_maps_invalid_algorithm_to_execute_failure() -> None: + """Tool validation failures must use the generic ExecuteResponse contract.""" + 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": "sha1"}}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "failed" + assert payload["result"] is None + assert "Unsupported checksum algorithm" in payload["message"] From 142360c95911ddef29b079d1954dfd14390c59ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:47:28 +0900 Subject: [PATCH 20/48] test(tools): require forged-session rejection and stable checksum error code --- backend/tests/test_content_checksum_api.py | 30 +++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py index 79bf5df83..46a25ae6c 100644 --- a/backend/tests/test_content_checksum_api.py +++ b/backend/tests/test_content_checksum_api.py @@ -60,6 +60,20 @@ def _signed_session_token() -> str: 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: @@ -89,8 +103,21 @@ def test_content_checksum_api_rejects_unauthenticated_request() -> None: assert response.json() == {"detail": "Authentication required"} +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 use the generic ExecuteResponse failure contract.""" + """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", @@ -102,4 +129,5 @@ def test_content_checksum_api_maps_invalid_algorithm_to_execute_failure() -> Non 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"] From 6fce51e1dece12307bc09f47ecfbb168dd9cdbf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:48:47 +0900 Subject: [PATCH 21/48] fix(tools): attach deterministic checksum validation codes --- backend/api/content_checksum_tool.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/api/content_checksum_tool.py b/backend/api/content_checksum_tool.py index d3da0134e..5aff4e266 100644 --- a/backend/api/content_checksum_tool.py +++ b/backend/api/content_checksum_tool.py @@ -20,6 +20,15 @@ ) +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() @@ -57,19 +66,24 @@ async def content_checksum_handler(params: dict[str, Any]) -> dict[str, Any]: length, encoding, and an authenticity warning. Raises: - ValueError: If the algorithm is not allowlisted or the encoded content - exceeds one MiB. + ContentChecksumError: If the algorithm is not allowlisted 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 ValueError( - "Unsupported checksum algorithm; choose sha256, sha3_256, or blake2b_256" + raise ContentChecksumError( + "Unsupported checksum algorithm; choose sha256, sha3_256, or blake2b_256", + error_code="unsupported_checksum_algorithm", ) payload = text.encode("utf-8") if len(payload) > MAX_CONTENT_BYTES: - raise ValueError(f"Content exceeds {MAX_CONTENT_BYTES} UTF-8 bytes") + raise ContentChecksumError( + f"Content exceeds {MAX_CONTENT_BYTES} UTF-8 bytes", + error_code="content_checksum_payload_too_large", + ) return { "algorithm_code": algorithm, From d9148355d2abe46d95524c1393117c1d7a049fd8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:50:21 +0900 Subject: [PATCH 22/48] fix(tools): surface stable error codes in execution envelope --- backend/api/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/api/tools.py b/backend/api/tools.py index bd15abfac..48db2f844 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -128,6 +128,9 @@ class ExecuteResponse(BaseModel): status: str = Field(..., description="실행 상태 (예: success, failed)") result: Any = Field(..., description="실행 결과 데이터") message: Optional[str] = Field(default=None, description="결과 메시지") + error_code: Optional[str] = Field( + default=None, description="예상된 실패의 안정적인 기계 판독 오류 코드" + ) class ToolRegistry: @@ -889,4 +892,5 @@ async def execute_tool(code: str, request: ExecuteRequest) -> ExecuteResponse: status="failed", result=None, message=_safe_tool_failure_message(e), + error_code=getattr(e, "error_code", None), ) From 9c2fb7b4facbd97771c5a23ef9a3e5d4a6cc67b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 07:50:44 +0900 Subject: [PATCH 23/48] test(tools): consolidate checksum API contract coverage --- .../test_content_checksum_api_contract.py | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 backend/tests/test_content_checksum_api_contract.py diff --git a/backend/tests/test_content_checksum_api_contract.py b/backend/tests/test_content_checksum_api_contract.py deleted file mode 100644 index 99977bafb..000000000 --- a/backend/tests/test_content_checksum_api_contract.py +++ /dev/null @@ -1,105 +0,0 @@ -"""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 JWT bytes using unpadded base64url.""" - return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") - - -def _signed_session_token() -> str: - """Build a short-lived session token accepted by the real auth dependency.""" - 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": "org-acme", - "groups": ["group-1"], - "workspace": "workspace-org-acme", - "exp": int(time.time()) + 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 test_checksum_execute_route_requires_authenticated_session() -> None: - """The private checksum endpoint must reject requests without auth.""" - 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"} - - -def test_checksum_execute_route_returns_authenticated_digest() -> None: - """Startup must expose the checksum tool through the documented envelope.""" - 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["message"] is None - assert payload["result"]["algorithm_code"] == "sha256" - assert payload["result"]["digest_hex"] == EXPECTED_SHA256_ABC - assert payload["result"]["byte_length"] == 3 - assert payload["result"]["encoding_code"] == "utf-8" - - -def test_checksum_execute_route_maps_invalid_algorithm_to_execute_failure() -> None: - """Tool validation failures must use the generic ExecuteResponse contract.""" - 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": "sha1"}}, - ) - - assert response.status_code == 200 - payload = response.json() - assert payload["status"] == "failed" - assert payload["result"] is None - assert "Unsupported checksum algorithm" in payload["message"] From 046bb0c7d37c723fc4b1a013ec8af150a48e8cfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 08:58:32 +0900 Subject: [PATCH 24/48] test(security): reject missing CSRF provenance --- backend/tests/test_main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 1852823f5..70e7e5f56 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -82,6 +82,16 @@ def test_state_changing_api_rejects_untrusted_origin(): assert response.json() == {"error_code": "csrf_origin_rejected"} +def test_state_changing_api_rejects_missing_origin_and_referer(): + response = client.put( + "/api/accounts/config", + json={"smtp_server": "mail.example.com"}, + ) + + assert response.status_code == 403 + assert response.json() == {"error_code": "csrf_referer_rejected"} + + def test_state_changing_api_allows_trusted_origin_to_reach_auth_gate(): response = client.put( "/api/accounts/config", From d269d50afbe1b7d696bad0ee8262ca5e1e6dab90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:01:04 +0900 Subject: [PATCH 25/48] fix(security): fail closed on missing CSRF provenance --- backend/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/main.py b/backend/main.py index 8e79752de..bb36910da 100644 --- a/backend/main.py +++ b/backend/main.py @@ -131,7 +131,7 @@ def _origin_from_referer(header_value: str | None) -> str | None: def _is_trusted_browser_origin(origin: str | None) -> bool: if origin is None: - return True + return False return origin in set(settings.ALLOWED_CORS_ORIGINS_LIST) @@ -159,7 +159,7 @@ async def reject_cross_site_state_changing_api_requests(request: Request, call_n status_code=403, content={"error_code": "csrf_origin_rejected"}, ) - if not _is_trusted_browser_origin(origin): + if origin is not None and not _is_trusted_browser_origin(origin): return JSONResponse( status_code=403, content={"error_code": "csrf_origin_rejected"}, From b454485662ad1e6c0759ad40e3e00a4e1d9a7eac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:03:59 +0900 Subject: [PATCH 26/48] fix(api): preserve tool response compatibility --- backend/api/tools.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/api/tools.py b/backend/api/tools.py index 48db2f844..257bab5fd 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -863,7 +863,11 @@ def delete_tool(code: str) -> None: registry.unregister(code) -@router.post("/tools/{code}/execute", response_model=ExecuteResponse) +@router.post( + "/tools/{code}/execute", + response_model=ExecuteResponse, + response_model_exclude_none=True, +) async def execute_tool(code: str, request: ExecuteRequest) -> ExecuteResponse: """ 특정 도구를 실행합니다. From 95a8c7ae2ae1e7f70ac0d3ef6fd8043beae3e215 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:45:54 +0900 Subject: [PATCH 27/48] fix(api): preserve null result while omitting absent error code --- backend/api/tools.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/api/tools.py b/backend/api/tools.py index 257bab5fd..ee5c9e6d9 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -20,7 +20,7 @@ ) from services.llm_provider_urls import build_pinned_https_async_client from fastapi import APIRouter, HTTPException -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SerializerFunctionWrapHandler, model_serializer router = APIRouter(prefix="/api", tags=["tools"]) logger = logging.getLogger(__name__) @@ -125,6 +125,8 @@ class ExecuteRequest(BaseModel): class ExecuteResponse(BaseModel): + """Stable public envelope returned by tool execution endpoints.""" + status: str = Field(..., description="실행 상태 (예: success, failed)") result: Any = Field(..., description="실행 결과 데이터") message: Optional[str] = Field(default=None, description="결과 메시지") @@ -132,6 +134,16 @@ class ExecuteResponse(BaseModel): default=None, description="예상된 실패의 안정적인 기계 판독 오류 코드" ) + @model_serializer(mode="wrap") + def _serialize_response( + self, handler: SerializerFunctionWrapHandler + ) -> dict[str, Any]: + """Omit an absent error code while preserving legacy null result fields.""" + payload = handler(self) + if self.error_code is None: + payload.pop("error_code", None) + return payload + class ToolRegistry: def __init__(self): @@ -863,11 +875,7 @@ def delete_tool(code: str) -> None: registry.unregister(code) -@router.post( - "/tools/{code}/execute", - response_model=ExecuteResponse, - response_model_exclude_none=True, -) +@router.post("/tools/{code}/execute", response_model=ExecuteResponse) async def execute_tool(code: str, request: ExecuteRequest) -> ExecuteResponse: """ 특정 도구를 실행합니다. @@ -897,4 +905,4 @@ async def execute_tool(code: str, request: ExecuteRequest) -> ExecuteResponse: result=None, message=_safe_tool_failure_message(e), error_code=getattr(e, "error_code", None), - ) + ) \ No newline at end of file From 7902fb66dccdfbc80be1a1b153f5fb454a48f0f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:41:10 +0900 Subject: [PATCH 28/48] test(security): distinguish browser CSRF from API clients --- backend/tests/test_main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 70e7e5f56..d9abaf499 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -82,9 +82,10 @@ def test_state_changing_api_rejects_untrusted_origin(): assert response.json() == {"error_code": "csrf_origin_rejected"} -def test_state_changing_api_rejects_missing_origin_and_referer(): +def test_browser_state_change_rejects_missing_origin_and_referer(): response = client.put( "/api/accounts/config", + headers={"Sec-Fetch-Site": "same-origin"}, json={"smtp_server": "mail.example.com"}, ) @@ -92,6 +93,16 @@ def test_state_changing_api_rejects_missing_origin_and_referer(): assert response.json() == {"error_code": "csrf_referer_rejected"} +def test_non_browser_state_change_without_provenance_reaches_auth_gate(): + response = client.put( + "/api/accounts/config", + json={"smtp_server": "mail.example.com"}, + ) + + assert response.status_code == 401 + assert response.json() == {"detail": "Authentication required"} + + def test_state_changing_api_allows_trusted_origin_to_reach_auth_gate(): response = client.put( "/api/accounts/config", From 77fd3c26f528155b2cf6bf94b05d2e0c5d698032 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:41:53 +0900 Subject: [PATCH 29/48] fix(security): scope CSRF provenance to browser requests --- backend/main.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index bb36910da..39f216e30 100644 --- a/backend/main.py +++ b/backend/main.py @@ -136,9 +136,15 @@ def _is_trusted_browser_origin(origin: str | None) -> bool: def _requires_browser_origin_check(request: Request) -> bool: - return ( - request.method.upper() in STATE_CHANGING_API_METHODS - and request.url.path.startswith("/api/") + """Return whether a state-changing API request carries browser provenance.""" + if ( + request.method.upper() not in STATE_CHANGING_API_METHODS + or not request.url.path.startswith("/api/") + ): + return False + return any( + request.headers.get(header_name) is not None + for header_name in ("origin", "referer", "sec-fetch-site") ) From 85678dc97af38d3fd023b90eb49d483ad03227e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 19:25:46 +0900 Subject: [PATCH 30/48] style: format checksum API coverage --- backend/api/tools.py | 5 +++-- backend/tests/test_content_checksum_api.py | 8 ++------ backend/tests/test_content_checksum_tool.py | 4 +++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/api/tools.py b/backend/api/tools.py index ee5c9e6d9..94766d740 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -721,6 +721,8 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]: "합니다", } ) + + def _normalize_analysis_text(value: str) -> str: """Normalize user text for deterministic, multilingual rule matching.""" if len(value) > ANALYSIS_TEXT_MAX_CHARS: @@ -784,7 +786,6 @@ async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]: ) - @router.get("/tools", response_model=list[ToolInfo]) def get_tools() -> list[ToolInfo]: """ @@ -905,4 +906,4 @@ async def execute_tool(code: str, request: ExecuteRequest) -> ExecuteResponse: result=None, message=_safe_tool_failure_message(e), error_code=getattr(e, "error_code", None), - ) \ No newline at end of file + ) diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py index 46a25ae6c..43a72e458 100644 --- a/backend/tests/test_content_checksum_api.py +++ b/backend/tests/test_content_checksum_api.py @@ -15,9 +15,7 @@ from main import app -EXPECTED_SHA256_ABC = ( - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" -) +EXPECTED_SHA256_ABC = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" def _base64url_encode(raw: bytes) -> str: @@ -69,9 +67,7 @@ def _tampered_session_token() -> str: base64.urlsafe_b64decode(signature_segment + signature_padding) ) signature[0] ^= 0x01 - return ( - f"{header_segment}.{payload_segment}.{_base64url_encode(bytes(signature))}" - ) + return f"{header_segment}.{payload_segment}.{_base64url_encode(bytes(signature))}" def test_content_checksum_api_executes_authenticated_request() -> None: diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 5388b19ae..429cf9606 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -72,7 +72,9 @@ async def test_content_checksum_generator_matches_published_vectors( @pytest.mark.asyncio -async def test_content_checksum_generator_hashes_exact_utf8_without_normalizing() -> None: +async def test_content_checksum_generator_hashes_exact_utf8_without_normalizing() -> ( + None +): """Canonically equivalent Unicode strings must remain distinct exact-byte inputs.""" composed = await registry.invoke_tool( "content_checksum_generator", From 5859a8f3f5e9dddf20a43313b53d7aa6453f8cd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 20:05:21 +0900 Subject: [PATCH 31/48] test(checksum): import application symbol explicitly --- backend/tests/test_content_checksum_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 429cf9606..3ea61ce31 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -2,9 +2,9 @@ import pytest -import main from api.content_checksum_tool import register_content_checksum_tool from api.tools import registry +from main import app SECURITY_NOTE = ( @@ -15,7 +15,7 @@ def test_application_bootstrap_registers_content_checksum_tool() -> None: """Loading the FastAPI application must expose the built-in checksum tool.""" - assert main.app is not None + assert app is not None assert registry.get("content_checksum_generator") is not None From 90988f660fb5b26d5169311ddf1b39d33185775f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:46:16 +0900 Subject: [PATCH 32/48] test(checksum): reject non-UTF-8 Unicode scalar input --- backend/tests/test_content_checksum_tool.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 3ea61ce31..7e8ea7225 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -2,7 +2,10 @@ import pytest -from api.content_checksum_tool import register_content_checksum_tool +from api.content_checksum_tool import ( + ContentChecksumError, + register_content_checksum_tool, +) from api.tools import registry from main import app @@ -96,6 +99,18 @@ async def test_content_checksum_generator_hashes_exact_utf8_without_normalizing( assert composed["digest_hex"] != decomposed["digest_hex"] +@pytest.mark.asyncio +async def test_content_checksum_generator_rejects_invalid_utf8_scalar_input() -> None: + """A lone surrogate must fail with the checksum tool's stable validation code.""" + with pytest.raises(ContentChecksumError) as exc_info: + await registry.invoke_tool( + "content_checksum_generator", + {"text": "\ud800", "algorithm": "sha256"}, + ) + + assert exc_info.value.error_code == "content_checksum_invalid_utf8" + + @pytest.mark.asyncio @pytest.mark.parametrize("algorithm", ["sha1", "md5", "SHA256", "sha-256", ""]) async def test_content_checksum_generator_rejects_unapproved_algorithm_names( From 228553ba730ab88d66723a5f8b2a6a7cd6694c21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:46:37 +0900 Subject: [PATCH 33/48] test(checksum): expose invalid UTF-8 as stable API failure --- backend/tests/test_content_checksum_api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py index 43a72e458..e677f57c7 100644 --- a/backend/tests/test_content_checksum_api.py +++ b/backend/tests/test_content_checksum_api.py @@ -127,3 +127,23 @@ def test_content_checksum_api_maps_invalid_algorithm_to_execute_failure() -> Non assert payload["result"] is None assert payload["error_code"] == "unsupported_checksum_algorithm" assert "Unsupported checksum algorithm" in payload["message"] + + +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" From dc480c3bc2294d351eef5fdaa61cbf80e0429b19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:47:13 +0900 Subject: [PATCH 34/48] fix(checksum): fail closed on invalid UTF-8 scalar input --- backend/api/content_checksum_tool.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/api/content_checksum_tool.py b/backend/api/content_checksum_tool.py index 5aff4e266..3076e5093 100644 --- a/backend/api/content_checksum_tool.py +++ b/backend/api/content_checksum_tool.py @@ -66,9 +66,10 @@ async def content_checksum_handler(params: dict[str, Any]) -> dict[str, Any]: length, encoding, and an authenticity warning. Raises: - ContentChecksumError: If the algorithm is not allowlisted or the - encoded content exceeds one MiB. Each expected failure carries a - stable machine-readable ``error_code``. + 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"] @@ -78,7 +79,13 @@ async def content_checksum_handler(params: dict[str, Any]) -> dict[str, Any]: error_code="unsupported_checksum_algorithm", ) - payload = text.encode("utf-8") + 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", From d6f8b31231709b90e464225ac7e79db155c52cef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:51:09 +0900 Subject: [PATCH 35/48] docs(checksum): refresh NIST status evidence --- docs/doctoring/content-checksum-generator.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md index 9cc301428..85814bfb5 100644 --- a/docs/doctoring/content-checksum-generator.md +++ b/docs/doctoring/content-checksum-generator.md @@ -16,9 +16,9 @@ MD5 and SHA-1 are intentionally absent from the normal surface. NIST states that A returned digest is an equality/integrity fingerprint for the exact supplied bytes. It does not authenticate a sender, prove provenance, or replace a keyed MAC or digital signature. Customer-facing output carries that warning with every result so the next action is explicit: compare the digest with an independently obtained expected digest when checking content equality; use an authenticated construction when sender or origin authenticity matters. -## Standards status reviewed 2026-08-15 +## Standards status reviewed 2026-09-10 -The official NIST publication page still lists FIPS 180-4 (2015) as the final Secure Hash Standard. NIST's March 7, 2023 Crypto Publication Review Board decision says FIPS 180-4 will be revised, including removal of the SHA-1 specification, but that decision is a revision plan rather than a replacement final standard. The official NIST publication page still lists FIPS 202 (2015) as the final SHA-3 standard. NIST's March 12, 2025 decision says FIPS 202 will be updated and SP 800-185 revised, with the normal draft/public-comment process to follow. No successor final publication was identified in the official status pages reviewed on 2026-08-15, so Naruon continues to cite the existing final standards while separately recording the announced updates. RFC 7693 remains the RFC Editor publication describing BLAKE2. +The official NIST publication page still lists FIPS 180-4 (2015) as the final Secure Hash Standard. NIST's March 7, 2023 Crypto Publication Review Board decision says FIPS 180-4 will be revised, including removal of the SHA-1 specification, but that decision is a revision plan rather than a replacement final standard. The official NIST/CSRC publication page still lists FIPS 202 (2015) as the final SHA-3 standard and carries a planning note that NIST decided to update it. NIST's March 12, 2025 decision says FIPS 202 will be updated and SP 800-185 revised through the normal draft/public-comment process. A fresh primary-source review on 2026-09-10 found no successor final publication, so Naruon continues to cite the existing final standards while separately recording the announced revisions. RFC 7693 remains the RFC Editor publication describing BLAKE2. The implementation uses Python's standard-library `hashlib` bindings only; this slice adds no external cryptographic dependency and no model-mediated decision path. Deterministic checksum behavior therefore remains independent of LLM judgment and credentials. @@ -33,7 +33,7 @@ No paper PDF is committed in this slice because redistribution permission for th ## Acceptance evidence -The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. +The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, rejection of text that cannot be represented as valid UTF-8 Unicode scalar values, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. ## References (APA 7th) From 515095728eee50600f6a561a1aab1f1a3b6a60f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:11:46 +0900 Subject: [PATCH 36/48] test(checksum): prove incremental chunk equivalence --- backend/tests/test_content_checksum_tool.py | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 7e8ea7225..a947ea709 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -1,5 +1,7 @@ """Regression tests for Naruon's bounded content-checksum tool.""" +import hashlib + import pytest from api.content_checksum_tool import ( @@ -74,6 +76,30 @@ async def test_content_checksum_generator_matches_published_vectors( } +@pytest.mark.asyncio +@pytest.mark.parametrize("algorithm", ["sha256", "sha3_256", "blake2b_256"]) +async def test_content_checksum_generator_matches_incremental_utf8_chunk_reference( + algorithm: str, +) -> None: + """One-shot tool output must equal incremental hashing of the same UTF-8 bytes.""" + chunks = ["Naruon ", "이메일 증거", "🙂\n", "second chunk"] + text = "".join(chunks) + if algorithm == "blake2b_256": + reference = hashlib.blake2b(digest_size=32) + else: + reference = hashlib.new(algorithm) + for chunk in chunks: + reference.update(chunk.encode("utf-8")) + + result = await registry.invoke_tool( + "content_checksum_generator", + {"text": text, "algorithm": algorithm}, + ) + + assert result["digest_hex"] == reference.hexdigest() + assert result["byte_length"] == len(text.encode("utf-8")) + + @pytest.mark.asyncio async def test_content_checksum_generator_hashes_exact_utf8_without_normalizing() -> ( None @@ -137,4 +163,4 @@ async def test_content_checksum_generator_enforces_utf8_byte_limit() -> None: await registry.invoke_tool( "content_checksum_generator", {"text": "é" * 524_289, "algorithm": "sha256"}, - ) + ) \ No newline at end of file From 030e870f645614a5e6522976c3c94eaf599ec263 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:12:10 +0900 Subject: [PATCH 37/48] docs(adr): keep checksum decision proposed until integration --- docs/adr/0007-bounded-content-checksum-surface.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/adr/0007-bounded-content-checksum-surface.md b/docs/adr/0007-bounded-content-checksum-surface.md index e307fbd4e..09fc592d2 100644 --- a/docs/adr/0007-bounded-content-checksum-surface.md +++ b/docs/adr/0007-bounded-content-checksum-surface.md @@ -1,12 +1,12 @@ # ADR-0007: Bound the customer content-checksum algorithm surface -**Status:** Accepted +**Status:** Proposed **Date:** 2026-08-15 **Decision owner:** Naruon maintainers -**Capability maturity:** deterministic tool contract; runtime availability remains subject to protected-branch integration and verification +**Capability maturity:** deterministic tool contract proposed on an unmerged branch; production authority requires protected-branch integration and verification **Scope:** Naruon's customer-facing deterministic content-checksum utility only. This ADR does not define sender authentication, signatures, password hashing, key derivation, or external artifact-signing policy. @@ -37,7 +37,9 @@ Naruon's deterministic tool registry already supplies a stable execution boundar ## Verification -The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. +The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, equivalence with incremental hashing of the same UTF-8 byte sequence across chunk boundaries, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. + +This ADR remains Proposed while the implementation is outside protected `develop`. It may be marked Accepted only after the decision and its exact implementation are normally integrated under the live protected-branch contract; a Draft PR or passing branch-local test suite is not acceptance authority. Standards status and APA 7 references are maintained in [`docs/doctoring/content-checksum-generator.md`](../doctoring/content-checksum-generator.md). @@ -46,4 +48,4 @@ Standards status and APA 7 references are maintained in [`docs/doctoring/content - **Expose every `hashlib` algorithm:** transfers a cryptographic policy decision to callers and makes legacy options look supported. - **Default to SHA-1 or MD5 for interoperability:** creates a new normal-surface dependency on algorithms Naruon should not recommend for security-labelled use. - **Normalize Unicode before hashing:** destroys the exact byte-level comparison contract and can make different source evidence converge silently. -- **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. +- **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. \ No newline at end of file From 18f14de149da4ad07ad353eb3addd3c2d23fec08 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:12:30 +0900 Subject: [PATCH 38/48] docs(adr): align checksum index with proposed state --- docs/adr/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 3b781bf2d..ba301df44 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -12,7 +12,7 @@ govern implementation. | [ADR-0001](0001-topic-measurement-authority.md) | Naruon-local policy for consuming structural topic measurement, never a keyword/label heuristic | Accepted | `ACCEPTED-NARUON-POLICY`; no runtime promotion | | [ADR-0002](0002-fitted-topic-artifact-consumption.md) | Conditionally consume only a versioned fitted topic artifact through a fail-closed adapter | Proposed | Target `PLANNED`; runtime `BLOCKED-UPSTREAM` | | [ADR-0003](0003-separate-topic-measurement-from-agenda-generation.md) | Keep statistical measurement separate from agenda generation | Proposed | Target and future capability `PLANNED`; no implementation authorization | -| [ADR-0007](0007-bounded-content-checksum-surface.md) | Bound the customer checksum surface to modern allowlisted algorithms and exact UTF-8 evidence | Accepted | Deterministic tool contract; runtime remains protected-integration gated | +| [ADR-0007](0007-bounded-content-checksum-surface.md) | Bound the customer checksum surface to modern allowlisted algorithms and exact UTF-8 evidence | Proposed | Deterministic tool contract pending protected integration and verification | | [ADR-0004](0004-status-weighted-calendar-conflicts.md) | Evaluate CalDAV VEVENT overlaps by occupying status; cancelled does not occupy | Accepted | `ACCEPTED-NARUON-POLICY`; advisory evaluate API only | The complete topic-intelligence requirements, architecture, contract, UML, @@ -28,4 +28,4 @@ Create or update an ADR when a Naruon change adopts or declines an external serv Every implementing PR must keep the corresponding source, tests, doctoring, architecture/operability contract, and CHANGELOG maturity truthful. An active PR, accepted local policy, or proposed target must not be described as protected- -branch implementation before it is integrated and independently verified. +branch implementation before it is integrated and independently verified. \ No newline at end of file From 1771b9da3337ba73bf13067361ae6ea96d89fcc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:14:32 +0900 Subject: [PATCH 39/48] docs(checksum): record chunk-equivalence evidence --- docs/doctoring/content-checksum-generator.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md index 85814bfb5..760ee4c4b 100644 --- a/docs/doctoring/content-checksum-generator.md +++ b/docs/doctoring/content-checksum-generator.md @@ -33,7 +33,7 @@ No paper PDF is committed in this slice because redistribution permission for th ## Acceptance evidence -The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, rejection of SHA-1/MD5 and ambiguous aliases, rejection of text that cannot be represented as valid UTF-8 Unicode scalar values, the one-MiB UTF-8 boundary, and idempotent application registration. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. +The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, **equivalence between the one-shot tool result and incremental hashing of the identical multilingual UTF-8 byte sequence across chunk boundaries for all three allowed algorithms**, rejection of SHA-1/MD5 and ambiguous aliases, rejection of text that cannot be represented as valid UTF-8 Unicode scalar values, the one-MiB UTF-8 boundary, and idempotent application registration. The chunk-equivalence regression is evidence about digest invariance for identical bytes; it does not introduce or claim a streaming public API. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. ## References (APA 7th) @@ -51,4 +51,4 @@ National Institute of Standards and Technology. (2023, March 7). *Decision to re National Institute of Standards and Technology. (2025, March 12). *Decision to update FIPS 202 and revise SP 800-185*. Computer Security Resource Center. https://csrc.nist.gov/News/2025/decision-to-update-fips-202-and-revise-sp-800-185 -Saarinen, M.-J. O., & Aumasson, J.-P. (2015). *The BLAKE2 cryptographic hash and message authentication code (MAC)* (RFC 7693). RFC Editor. https://doi.org/10.17487/RFC7693 +Saarinen, M.-J. O., & Aumasson, J.-P. (2015). *The BLAKE2 cryptographic hash and message authentication code (MAC)* (RFC 7693). RFC Editor. https://doi.org/10.17487/RFC7693 \ No newline at end of file From c7a26703e73c5f0bcacc719d55ac476dd5394873 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 19:42:36 +0900 Subject: [PATCH 40/48] refactor(checksum): hand browser provenance CSRF delta to #1706 --- backend/main.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/main.py b/backend/main.py index a4304302d..6a854e869 100644 --- a/backend/main.py +++ b/backend/main.py @@ -132,20 +132,14 @@ def _origin_from_referer(header_value: str | None) -> str | None: def _is_trusted_browser_origin(origin: str | None) -> bool: if origin is None: - return False + return True return origin in set(settings.ALLOWED_CORS_ORIGINS_LIST) def _requires_browser_origin_check(request: Request) -> bool: - """Return whether a state-changing API request carries browser provenance.""" - if ( - request.method.upper() not in STATE_CHANGING_API_METHODS - or not request.url.path.startswith("/api/") - ): - return False - return any( - request.headers.get(header_name) is not None - for header_name in ("origin", "referer", "sec-fetch-site") + return ( + request.method.upper() in STATE_CHANGING_API_METHODS + and request.url.path.startswith("/api/") ) @@ -166,7 +160,7 @@ async def reject_cross_site_state_changing_api_requests(request: Request, call_n status_code=403, content={"error_code": "csrf_origin_rejected"}, ) - if origin is not None and not _is_trusted_browser_origin(origin): + if not _is_trusted_browser_origin(origin): return JSONResponse( status_code=403, content={"error_code": "csrf_origin_rejected"}, From b98dbf984b7128aa0412e318c7590a7eea5d9717 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 19:42:54 +0900 Subject: [PATCH 41/48] refactor(checksum): remove CSRF regression now owned by #1706 --- backend/tests/test_main.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index d9abaf499..1852823f5 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -82,27 +82,6 @@ def test_state_changing_api_rejects_untrusted_origin(): assert response.json() == {"error_code": "csrf_origin_rejected"} -def test_browser_state_change_rejects_missing_origin_and_referer(): - response = client.put( - "/api/accounts/config", - headers={"Sec-Fetch-Site": "same-origin"}, - json={"smtp_server": "mail.example.com"}, - ) - - assert response.status_code == 403 - assert response.json() == {"error_code": "csrf_referer_rejected"} - - -def test_non_browser_state_change_without_provenance_reaches_auth_gate(): - response = client.put( - "/api/accounts/config", - json={"smtp_server": "mail.example.com"}, - ) - - assert response.status_code == 401 - assert response.json() == {"detail": "Authentication required"} - - def test_state_changing_api_allows_trusted_origin_to_reach_auth_gate(): response = client.put( "/api/accounts/config", From 5c42f5270d9424039d58f0a30ef1766b4ed22ab0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 19:50:06 +0900 Subject: [PATCH 42/48] test(checksum): adopt empty-input regression from generated duplicate #1707 --- backend/tests/test_content_checksum_tool.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index a947ea709..3714353f8 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -76,6 +76,23 @@ async def test_content_checksum_generator_matches_published_vectors( } +@pytest.mark.asyncio +async def test_content_checksum_generator_hashes_empty_sha256_input() -> None: + """Empty UTF-8 content is valid input and must produce the published SHA-256 digest.""" + result = await registry.invoke_tool( + "content_checksum_generator", + {"text": "", "algorithm": "sha256"}, + ) + + assert result == { + "algorithm_code": "sha256", + "digest_hex": hashlib.sha256(b"").hexdigest(), + "byte_length": 0, + "encoding_code": "utf-8", + "security_note": SECURITY_NOTE, + } + + @pytest.mark.asyncio @pytest.mark.parametrize("algorithm", ["sha256", "sha3_256", "blake2b_256"]) async def test_content_checksum_generator_matches_incremental_utf8_chunk_reference( @@ -163,4 +180,4 @@ async def test_content_checksum_generator_enforces_utf8_byte_limit() -> None: await registry.invoke_tool( "content_checksum_generator", {"text": "é" * 524_289, "algorithm": "sha256"}, - ) \ No newline at end of file + ) From 6bf2989d571a2aa94ce1f92997650cc450e88e9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 21:39:30 +0900 Subject: [PATCH 43/48] test(checksum): strengthen published-vector and UTF-8 chunk evidence --- backend/tests/test_content_checksum_tool.py | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 3714353f8..624304026 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -86,7 +86,10 @@ async def test_content_checksum_generator_hashes_empty_sha256_input() -> None: assert result == { "algorithm_code": "sha256", - "digest_hex": hashlib.sha256(b"").hexdigest(), + "digest_hex": ( + "e3b0c44298fc1c149afbf4c8996fb924" + "27ae41e4649b934ca495991b7852b855" + ), "byte_length": 0, "encoding_code": "utf-8", "security_note": SECURITY_NOTE, @@ -98,15 +101,22 @@ async def test_content_checksum_generator_hashes_empty_sha256_input() -> None: async def test_content_checksum_generator_matches_incremental_utf8_chunk_reference( algorithm: str, ) -> None: - """One-shot tool output must equal incremental hashing of the same UTF-8 bytes.""" - chunks = ["Naruon ", "이메일 증거", "🙂\n", "second chunk"] - text = "".join(chunks) + """One-shot tool output must match byte chunks split inside UTF-8 code points.""" + payload = "Naruon 이메일 증거🙂\nsecond chunk".encode("utf-8") + multibyte_start = payload.index("이".encode("utf-8")) + emoji_start = payload.index("🙂".encode("utf-8")) + chunks = [ + payload[: multibyte_start + 1], + payload[multibyte_start + 1 : emoji_start + 2], + payload[emoji_start + 2 :], + ] + text = b"".join(chunks).decode("utf-8") if algorithm == "blake2b_256": reference = hashlib.blake2b(digest_size=32) else: reference = hashlib.new(algorithm) for chunk in chunks: - reference.update(chunk.encode("utf-8")) + reference.update(chunk) result = await registry.invoke_tool( "content_checksum_generator", @@ -114,7 +124,7 @@ async def test_content_checksum_generator_matches_incremental_utf8_chunk_referen ) assert result["digest_hex"] == reference.hexdigest() - assert result["byte_length"] == len(text.encode("utf-8")) + assert result["byte_length"] == len(payload) @pytest.mark.asyncio From d23b6f82d61de219ceb6fc0eec7781d810a580f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 22 Sep 2026 09:47:17 +0900 Subject: [PATCH 44/48] test(checksum): reproduce advertised algorithm-label mismatch --- backend/tests/test_content_checksum_tool.py | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_content_checksum_tool.py b/backend/tests/test_content_checksum_tool.py index 624304026..91531025f 100644 --- a/backend/tests/test_content_checksum_tool.py +++ b/backend/tests/test_content_checksum_tool.py @@ -76,6 +76,29 @@ async def test_content_checksum_generator_matches_published_vectors( } +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("algorithm", "expected_code"), + [ + ("SHA-256", "sha256"), + ("SHA-3-256", "sha3_256"), + ("SHA3-256", "sha3_256"), + ("BLAKE2b-256", "blake2b_256"), + ], +) +async def test_content_checksum_generator_accepts_advertised_algorithm_labels( + algorithm: str, + expected_code: str, +) -> None: + """Human-facing algorithm labels in the catalog must execute as canonical codes.""" + result = await registry.invoke_tool( + "content_checksum_generator", + {"text": "abc", "algorithm": algorithm}, + ) + + assert result["algorithm_code"] == expected_code + + @pytest.mark.asyncio async def test_content_checksum_generator_hashes_empty_sha256_input() -> None: """Empty UTF-8 content is valid input and must produce the published SHA-256 digest.""" @@ -165,11 +188,11 @@ async def test_content_checksum_generator_rejects_invalid_utf8_scalar_input() -> @pytest.mark.asyncio -@pytest.mark.parametrize("algorithm", ["sha1", "md5", "SHA256", "sha-256", ""]) +@pytest.mark.parametrize("algorithm", ["sha1", "md5", "sha512", "blake2b", ""]) async def test_content_checksum_generator_rejects_unapproved_algorithm_names( algorithm: str, ) -> None: - """Legacy or ambiguous algorithm names must fail closed instead of being guessed.""" + """Legacy or out-of-contract algorithm names must fail closed.""" with pytest.raises(ValueError, match="Unsupported checksum algorithm"): await registry.invoke_tool( "content_checksum_generator", @@ -190,4 +213,4 @@ async def test_content_checksum_generator_enforces_utf8_byte_limit() -> None: await registry.invoke_tool( "content_checksum_generator", {"text": "é" * 524_289, "algorithm": "sha256"}, - ) + ) \ No newline at end of file From e1a0d464fcfcecca7e97819978fd183b995f6b7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 22 Sep 2026 09:47:37 +0900 Subject: [PATCH 45/48] fix(checksum): accept documented algorithm labels --- backend/api/content_checksum_tool.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/backend/api/content_checksum_tool.py b/backend/api/content_checksum_tool.py index 3076e5093..c6a081df4 100644 --- a/backend/api/content_checksum_tool.py +++ b/backend/api/content_checksum_tool.py @@ -50,20 +50,35 @@ def _blake2b_256(payload: bytes) -> str: "blake2b_256": _blake2b_256, } +_ALGORITHM_ALIASES = { + "sha-256": "sha256", + "sha3-256": "sha3_256", + "sha-3-256": "sha3_256", + "blake2b-256": "blake2b_256", +} + + +def _normalize_algorithm_code(value: str) -> str: + """Map documented human-facing labels to the bounded canonical codes.""" + normalized = value.lower() + return _ALGORITHM_ALIASES.get(normalized, normalized) + 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. + UTF-8 encoding are rejected before hashing. Canonical algorithm codes and + the documented SHA/BLAKE2 display labels resolve to the same bounded + allowlist; legacy or out-of-contract algorithms still fail closed. 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. + A deterministic checksum receipt with the canonical algorithm code, + digest, byte length, encoding, and an authenticity warning. Raises: ContentChecksumError: If the algorithm is not allowlisted, the text @@ -72,7 +87,7 @@ async def content_checksum_handler(params: dict[str, Any]) -> dict[str, Any]: ``error_code``. """ text = params["text"] - algorithm = params["algorithm"] + algorithm = _normalize_algorithm_code(params["algorithm"]) if algorithm not in _HASHERS: raise ContentChecksumError( "Unsupported checksum algorithm; choose sha256, sha3_256, or blake2b_256", @@ -119,4 +134,4 @@ def register_content_checksum_tool() -> None: parameters={"text": "string", "algorithm": "string"}, ), content_checksum_handler, - ) + ) \ No newline at end of file From d0edb517759e3aeaff115e5f449b0f23d1b90cc6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 22 Sep 2026 09:48:25 +0900 Subject: [PATCH 46/48] test(checksum): cover catalog label through execute API --- backend/tests/test_content_checksum_api.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_content_checksum_api.py b/backend/tests/test_content_checksum_api.py index e677f57c7..eefbd4a21 100644 --- a/backend/tests/test_content_checksum_api.py +++ b/backend/tests/test_content_checksum_api.py @@ -87,6 +87,22 @@ def test_content_checksum_api_executes_authenticated_request() -> None: assert payload["message"] == "Execution successful" +def test_content_checksum_api_accepts_advertised_sha256_label() -> None: + """The catalog's human-facing SHA-256 label must execute and return the canonical 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": "SHA-256"}}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "success" + assert payload["result"]["algorithm_code"] == "sha256" + assert payload["result"]["digest_hex"] == EXPECTED_SHA256_ABC + + 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: @@ -146,4 +162,4 @@ def test_content_checksum_api_maps_invalid_utf8_to_execute_failure() -> None: payload = response.json() assert payload["status"] == "failed" assert payload["result"] is None - assert payload["error_code"] == "content_checksum_invalid_utf8" + assert payload["error_code"] == "content_checksum_invalid_utf8" \ No newline at end of file From 3848261ef0066449be4048dd9a58fb2b56bef9a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 22 Sep 2026 09:48:45 +0900 Subject: [PATCH 47/48] docs(checksum): record bounded display-label normalization --- .../0007-bounded-content-checksum-surface.md | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/adr/0007-bounded-content-checksum-surface.md b/docs/adr/0007-bounded-content-checksum-surface.md index 09fc592d2..893a3f6bc 100644 --- a/docs/adr/0007-bounded-content-checksum-surface.md +++ b/docs/adr/0007-bounded-content-checksum-surface.md @@ -18,18 +18,23 @@ A checksum utility is useful for comparing exported email/document text, audit e Naruon's deterministic tool registry already supplies a stable execution boundary. The smallest defensible slice is therefore an explicit modern algorithm allowlist with bounded exact-byte input and a machine-visible authenticity warning, rather than a wrapper over arbitrary `hashlib` names. +A later review exposed a presentation mismatch: the catalog described standard human-facing names such as `SHA-256`, but the handler accepted only internal codes such as `sha256`. A user entering the advertised label could therefore receive an unsupported-algorithm failure even though they had selected the intended algorithm. The repair treats only the documented display spellings as aliases of the same three algorithms; it does not expand the cryptographic surface. + ## Decision -1. The normal customer surface accepts exactly `sha256`, `sha3_256`, and `blake2b_256`. -2. MD5, SHA-1, aliases, case variants, and unknown names fail closed; no compatibility guessing is performed. -3. Text is encoded as UTF-8 exactly as supplied and is not Unicode-normalized before hashing. -4. One invocation accepts at most 1,048,576 encoded bytes so the generic tool endpoint cannot become an unbounded hashing sink. -5. The result records the selected algorithm, hexadecimal digest, encoded byte length, encoding, and an explicit warning that the digest does not authenticate a sender or replace a MAC/signature. -6. The implementation uses the Python standard library and remains deterministic and independent of model judgment or LLM credentials. -7. A future legacy compatibility mode requires a separate reviewed decision with an explicit non-security acknowledgement; this ADR does not authorize one. +1. The normal customer surface contains exactly SHA-256, SHA3-256, and BLAKE2b-256. Canonical response codes remain `sha256`, `sha3_256`, and `blake2b_256`. +2. Input accepts those canonical codes case-insensitively plus the documented display spellings `SHA-256`, `SHA3-256`/`SHA-3-256`, and `BLAKE2b-256`; each display spelling is normalized to the corresponding canonical response code. +3. MD5, SHA-1, SHA-512, generic BLAKE2b, unknown names, and unlisted aliases fail closed. No free-form `hashlib` name guessing is performed. +4. Text is encoded as UTF-8 exactly as supplied and is not Unicode-normalized before hashing. +5. One invocation accepts at most 1,048,576 encoded bytes so the generic tool endpoint cannot become an unbounded hashing sink. +6. The result records the canonical algorithm code, hexadecimal digest, encoded byte length, encoding, and an explicit warning that the digest does not authenticate a sender or replace a MAC/signature. +7. The implementation uses the Python standard library and remains deterministic and independent of model judgment or LLM credentials. +8. A future legacy compatibility mode requires a separate reviewed decision with an explicit non-security acknowledgement; this ADR does not authorize one. ## Consequences +- Buyers can enter the algorithm spelling shown by the catalog without weakening the underlying allowlist. +- API consumers receive one stable canonical algorithm code regardless of the accepted display spelling. - Buyers can compare exact content evidence without being steered toward a legacy digest. - Canonically equivalent Unicode strings may intentionally produce different digests when their UTF-8 byte sequences differ; this is correct for exact-byte evidence. - Callers that need origin authenticity must select an authenticated construction outside this tool. @@ -37,7 +42,7 @@ Naruon's deterministic tool registry already supplies a stable execution boundar ## Verification -The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, equivalence with incremental hashing of the same UTF-8 byte sequence across chunk boundaries, rejection of non-allowlisted names, byte-boundary tests, idempotent registry startup, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. +The implementation contract requires stable vectors for all three algorithms, exact UTF-8 behavior, equivalence with incremental hashing of the same UTF-8 byte sequence across chunk boundaries, standard display-label to canonical-code normalization, rejection of legacy/out-of-contract names, byte-boundary tests, idempotent registry startup, authenticated API coverage for at least one advertised label, 100% owned production statement/branch coverage where exposed, and current-head security/review gates before protected integration. This ADR remains Proposed while the implementation is outside protected `develop`. It may be marked Accepted only after the decision and its exact implementation are normally integrated under the live protected-branch contract; a Draft PR or passing branch-local test suite is not acceptance authority. @@ -46,6 +51,8 @@ Standards status and APA 7 references are maintained in [`docs/doctoring/content ## Alternatives rejected - **Expose every `hashlib` algorithm:** transfers a cryptographic policy decision to callers and makes legacy options look supported. +- **Reject the catalog's documented display spellings:** leaves the product UI/API contract internally inconsistent and forces users to infer undocumented implementation codes. +- **Accept arbitrary punctuation/alias variants:** turns a bounded product contract back into compatibility guessing; only explicitly documented spellings are normalized. - **Default to SHA-1 or MD5 for interoperability:** creates a new normal-surface dependency on algorithms Naruon should not recommend for security-labelled use. - **Normalize Unicode before hashing:** destroys the exact byte-level comparison contract and can make different source evidence converge silently. - **Describe a digest as authentication:** an unkeyed checksum does not establish sender or provenance identity. \ No newline at end of file From 6eb3f28e57b5fa57c530bc7ded0d244a7a3605c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 22 Sep 2026 09:49:20 +0900 Subject: [PATCH 48/48] docs(checksum): trace catalog label repair --- docs/doctoring/content-checksum-generator.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/content-checksum-generator.md b/docs/doctoring/content-checksum-generator.md index 760ee4c4b..c56b32c91 100644 --- a/docs/doctoring/content-checksum-generator.md +++ b/docs/doctoring/content-checksum-generator.md @@ -8,14 +8,24 @@ Naruon's `content_checksum_generator` compares the exact UTF-8 byte sequence supplied by the caller. It deliberately does **not** Unicode-normalize input before hashing, because normalization would change the byte-level evidence being compared. The deterministic tool accepts at most 1,048,576 UTF-8 bytes per invocation and exposes only these normal-surface algorithms: -- `sha256` — SHA-256 from FIPS 180-4; -- `sha3_256` — SHA3-256 from FIPS 202; -- `blake2b_256` — BLAKE2b with a 256-bit digest, using the BLAKE2 construction standardized in RFC 7693. +- canonical `sha256`, with documented display spelling `SHA-256`; +- canonical `sha3_256`, with documented display spellings `SHA3-256` and `SHA-3-256`; +- canonical `blake2b_256`, with documented display spelling `BLAKE2b-256`. + +Input matching for these canonical/display spellings is case-insensitive, and output always returns the canonical code. The mapping is an explicit presentation compatibility boundary, not free-form `hashlib` alias resolution. SHA-512, generic BLAKE2b, MD5, SHA-1 and other unlisted names remain outside the normal surface. MD5 and SHA-1 are intentionally absent from the normal surface. NIST states that SHA-1 is being transitioned out for applying cryptographic protection by December 31, 2030; Naruon therefore does not introduce SHA-1 as a new customer-facing checksum choice. No legacy-compatibility checksum mode is part of this slice. A returned digest is an equality/integrity fingerprint for the exact supplied bytes. It does not authenticate a sender, prove provenance, or replace a keyed MAC or digital signature. Customer-facing output carries that warning with every result so the next action is explicit: compare the digest with an independently obtained expected digest when checking content equality; use an authenticated construction when sender or origin authenticity matters. +## Catalog/API label mismatch finding and repair + +Generated hash-tool provenance PR #1739 carried a review finding that human-facing algorithm names could be rejected by the execution boundary. Fresh inspection confirmed the same narrower mismatch on the canonical checksum owner: the catalog description advertised `SHA-256`, `SHA-3-256`, and `BLAKE2b-256`, while the handler accepted only internal codes. + +The repair is deliberately bounded. Regression `d23b6f82d61de219ceb6fc0eec7781d810a580f4` requires documented display labels to resolve to canonical codes. Production fix `e1a0d464fcfcecca7e97819978fd183b995f6b7d` adds only the explicit label map and canonicalization. API regression `d0edb517759e3aeaff115e5f449b0f23d1b90cc6` proves `SHA-256` through the authenticated execute route. MD5, SHA-1, SHA-512, generic BLAKE2b, unknown names, and unlisted aliases still fail closed. + +The broader generated-PR review finding that the protected Tools page fabricated placeholder `test_value` parameters is owned by the canonical utility-console form lane #1505, which replaces placeholder execution with real user-entered parameter values. This checksum owner does not copy or parallel-write that frontend. Final buyer-visible acceptance therefore requires the utility-console owner path and this checksum owner to coexist on the integrated tree before browser/UI completion can be claimed. + ## Standards status reviewed 2026-09-10 The official NIST publication page still lists FIPS 180-4 (2015) as the final Secure Hash Standard. NIST's March 7, 2023 Crypto Publication Review Board decision says FIPS 180-4 will be revised, including removal of the SHA-1 specification, but that decision is a revision plan rather than a replacement final standard. The official NIST/CSRC publication page still lists FIPS 202 (2015) as the final SHA-3 standard and carries a planning note that NIST decided to update it. NIST's March 12, 2025 decision says FIPS 202 will be updated and SP 800-185 revised through the normal draft/public-comment process. A fresh primary-source review on 2026-09-10 found no successor final publication, so Naruon continues to cite the existing final standards while separately recording the announced revisions. RFC 7693 remains the RFC Editor publication describing BLAKE2. @@ -33,7 +43,7 @@ No paper PDF is committed in this slice because redistribution permission for th ## Acceptance evidence -The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, **equivalence between the one-shot tool result and incremental hashing of the identical multilingual UTF-8 byte sequence across chunk boundaries for all three allowed algorithms**, rejection of SHA-1/MD5 and ambiguous aliases, rejection of text that cannot be represented as valid UTF-8 Unicode scalar values, the one-MiB UTF-8 boundary, and idempotent application registration. The chunk-equivalence regression is evidence about digest invariance for identical bytes; it does not introduce or claim a streaming public API. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, and independent review gates before the capability may be described as shipped. +The regression contract covers published/stable `abc` digest vectors for all three algorithms, exact-byte distinction between canonically equivalent Unicode strings, **equivalence between the one-shot tool result and incremental hashing of the identical multilingual UTF-8 byte sequence across chunk boundaries for all three allowed algorithms**, documented display-label normalization to canonical codes, rejection of legacy/out-of-contract names, rejection of text that cannot be represented as valid UTF-8 Unicode scalar values, the one-MiB UTF-8 boundary, authenticated API execution for an advertised label, and idempotent application registration. The chunk-equivalence regression is evidence about digest invariance for identical bytes; it does not introduce or claim a streaming public API. Protected-branch integration still requires exact-current-head CI, security, **100% owned production statement/branch coverage where exposed as required by [ADR-0007](../adr/0007-bounded-content-checksum-surface.md)**, independent review gates, and integration with #1505's real parameter-entry UI before the capability may be described as shipped. ## References (APA 7th)