Task: Bring ores.security to the clean standard
Table of Contents
This page documents a task in the Clean ores.security to the component clean standard story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
ores.security passes the Component Clean Standard for a component of kind All.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Clean ores.security to the component clean standard |
| Now | Merged as `b2e6c069db` in PR #2162. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-26 |
3. Acceptance
- Every checklist item that applies to a component of kind All passes, with its evidence recorded on the task.
- Every item that does not apply is recorded as not applicable, with the reason.
- Every generator defect found is fixed at its source, with a regression test.
- ores.security is listed in
COMPONENTS_UNDER_TEST, andcheck_component_drift.py --allpasses.
4. Plan
Work the Component Clean Standard in its section order, one entity at a time, and record each item's result in the table below.
- Baseline and inventory (B01 to B05). Record the baseline before any edit.
- Models (M01 to M10).
- Protocol (P01 to P06).
- Generation (G01 to G06).
- Wiring and data (W01 to W04).
- Shell (S01, S02).
- Structure and hygiene (H01 to H03).
- Verification (V01 to V06), ending with the registry entry.
| Item | Result | Evidence |
|---|---|---|
| B01 | pass | Baseline before any edit, on `feature/clean-security` at `230006c221`. Build 0 at 100 percent. `ores.security.tests` passes at 127 assertions in 55 test cases. `regenerate_cmake_component_files.py –component ores.security –check` reports 2 stale files, `src/component_files.cmake` and `tests/component_files.cmake`. No catalogue row, so the drift gate cannot name the component. |
| B02 | pass | 0 entity, junction, operation and message models. |
| B03 | pass | 16 production C++ files at the survey, 11 headers and 5 sources, plus 6 files under `tests/`. The component now holds 14 production files, 10 headers and 4 sources, and 5 files under `tests/`, of which 4 are suites and one is `main.cpp`. The consumer census is unchanged by this pass: `jwt_authenticator.hpp` reaches 441 C++ files outside the component, `jwt_claims.hpp` 7, `password_hasher.hpp` 4, `jwt_error.hpp` 2, and the two validators 2 each. The root namespace header has no consumer by design; see G08. |
| B04 | not applicable | The component serves and sends no NATS subject and has no protocol header. |
| B05 | pass | This story is the only live capture naming the component. |
| M01 to M05, M07 to M09 | not applicable | No org model. |
| M06 | not applicable | No entity and no hand-written wire type. The only wire artefacts are the JWTs the component signs, and jwt-cpp owns their shape. |
| M10 | fixed | `component_overview.org` advertised AES-256-GCM and PBKDF2 for a facet this pass deletes, gave the component no consumer, and carried no record of either policy surface. It is rewritten: the facets match the tree, the consumer census is stated, the scrypt cost policy and the JWT constraints are described, the four findings below are recorded, and `See also` is filled. The root namespace header keeps its role and its claims are corrected in place. |
| P01 to P06 | not applicable | No entity and no protocol. |
| G01 | not applicable | No model, so nothing regenerates from one. |
| G02 | fixed | The dead `crypto/encryption` pair is deleted with its test and the two never-defined `base64` declarations in `password_hasher.hpp` are deleted. The root namespace header is kept: it is the component's doxygen namespace documentation and the source the diagram conventions read, which G08 of the standard now states explicitly. `jwt_error::missing_claims` is kept too, and now has a producer: the survey proposed removing it because nothing returned it, but nothing returned any of the specific codes, because the mapping could not see them. |
| G03 | fixed | The two `component_files.cmake` files now carry the generated marker. |
| G04 | not applicable | No generated code. |
| G05 | fixed | The `generate_component_puml.py` defects are fixed on the two earlier passes; this component picks the fixes up. The refresh drops the deleted types and keeps the manual block. |
| G06 | fixed | The same two files as G03, now current. |
| W01 to W04 | not applicable | No entity, registrar, handler, populate script or SQL script. |
| S01, S02 | not applicable | No entity and no shell unit. |
| H01 | pass | `validate_docs.py` reports only the pre-existing `ores.shell.iam` finding. Every file and type name snake_case, the CMake lists match the tree, no commented-out code, no stub header left. The rendered diagram was read: every box carries its API, `boost_json` shows the type aliases the parser cannot see, and every edge is one the code supports. |
| H02 | fixed | The dead encryption pair, the two undefined declarations, the umbrella stub, and about fifteen narration comments that went with the deleted file. |
| H03 | fixed | The end-of-line comment on the scrypt bounds is gone, and the five comments in `email_validator.cpp` plus the two restatements in `password_hasher.cpp` that repeated the next line are gone. |
| V01 | pass | Whole-tree build 0 at 100 percent. |
| V02 | fixed | The suite went from 127 assertions in 55 test cases to 149 in 57. Ten cases arrived and the eight encryption cases left with their file: a downgraded scrypt cost is refused while an independently built hash at the floor is accepted, the stored format pins ln=14 with the fast flag absent and ln=10 with it set, empty verify input is refused, and the JWT paths cover a wrong issuer, a wrong audience, a missing claim, the allow-expired leeway on both the accepting and the rejecting side, and the RS256 verifier against an HS256 token signed with the public key. |
| V03, V04 | not applicable | No entity, no SQL and no generated shell command. |
| V05 | pass | Every gate green on the committed head: `regenerate_cmake_component_files.py –component ores.security –check`, `check_component_drift.py –all`, `validate_docs.py`, `./compass.sh lint`, and `clang-format –dry-run –Werror` on every changed and added file. |
| V06 | not applicable | No catalogue row and no models, so the component cannot join `COMPONENTS_UNDER_TEST`. Recorded as the programme gap. |
5. Notes
Survey, 2026-09-25, at trunk `5de59e3fb8`, by an agent that did not write the component. Cryptographic code, so an untested path here carries more weight than an untested path elsewhere.
- Scale. 16 production C++ files in 11 headers and 5 sources, with 6 files under `tests/`. 55 test cases, 127 assertions, and 448 files outside the component include it. Consumers include `ores.iam`'s account and signup services and its auth handler.
- Suites pass. `ores.security.tests` runs green, 127 assertions in 55 test cases.
- Keep the component. It is the repo's only home for hashing, JWT and the OWASP validators.
The survey was checked against the tree before any edit. Eight of its findings held as written. Two did not: `encryption` turned out to have no reference anywhere, not even its own test being a consumer of anything but the test, and the `jwt_error::missing_claims` enumerator is not so much dead as unreachable, which is a different defect with a different fix.
6. Work items
| Item | Outcome |
|---|---|
| 1. Decide `crypto/encryption.cpp` | deleted with its test. It carried the only AES-256-GCM and PBKDF2 surface, 193 lines and three public statics, and no file in the tree included it. Its own test was the only consumer. |
| 2. Delete the undefined `base64` declarations | done in the same commit as item 1. They were private declarations with no definition and no caller. |
| 3. `jwt_error::missing_claims` | kept, and given a producer. The defect was not the enumerator but the mapping: every specific code was unreachable because the mapping searched the exception text for words jwt-cpp does not use. Both validation paths now map from jwt-cpp's error code, so `missing_claims` is returned for a token that omits a claim the verifier requires. |
| 4. Forbid a downgraded scrypt cost | done. Verification refuses a stored hash weaker than the cost the running build produces, and the test proves it by building a valid hash at ln=1 independently with OpenSSL and showing the floor rejects it while accepting the same construction at ln=10. |
| 5. Pin the stored format and the cost | done. The test pins `$scrypt\(ln=14,r=8,p=1\)` with the fast flag absent and `$scrypt$ln=10,…` with it set, which required removing the function-local cache that made the production cost unreachable after the first call. |
| 6. Test `validate_allow_expired` | done, and it found a gap: the path relaxed the expiry and, in doing so, skipped the audience check `validate` applies, so a token minted for another service was accepted. It checks the audience now, and three cases cover the leeway's accepting and rejecting sides. |
| 7. Failure paths for `decrypt` | not applicable: `decrypt` left with `encryption`. |
| 8. The JWT cases | done for algorithm confusion, wrong issuer, wrong audience and missing claims. The `nbf` case cannot be written: `create_token` sets only `iat` and `exp`, so the not-yet-valid path is unreachable through this API. Recorded on the overview rather than faked in a test. |
| 9. Empty-input `verify_password` | not applicable: `verify_password` was a method of the deleted `encryption` class. The surviving `password_hasher::verify` already refuses an empty password and an empty hash, and a test now pins that. |
| 10. Hygiene | done. The narration went with the deleted file, the end-of-line comment is gone, and the seven restatements in `email_validator.cpp` and `password_hasher.cpp` are gone. The overview is rewritten against the tree. |
7. Deletions
| Artefact | Files | Lines | Why it goes |
|---|---|---|---|
| `crypto/encryption.hpp` and `encryption.cpp` | 2 | 274 | No reference anywhere in the tree. The only AES-256-GCM and PBKDF2 surface the component had, and nothing asked for it. |
| `tests/crypto_encryption_tests.cpp` | 1 | 158 | The deleted class's only consumer. |
| the two `base64` declarations | — | 10 | Declared, never defined, never called. |
Three files and 432 lines left the component, and the suite lost the eight cases that covered the deleted class.
`ores.security.hpp` is not in this table. The first cut of this pass deleted it as a consumer-less stub, and that was wrong: every component keeps a namespace documentation header, its content is an empty namespace plus the `@brief` doxygen renders, and the diagram conventions read that `@brief` when they label the component. The standard's G02 said dead headers go and did not exempt them; G08 now says so explicitly, and the header is restored with its claims corrected. The same mistake was made on the ores.utility pass, where eleven namespace headers were deleted, and is corrected there separately.
8. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
9. PRs
| PR | Title |
|---|---|
| #2162 | [security] Clean ores.security to the component clean standard |
10. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | `validate` and `validate_allow_expired` reported every JWT failure as `invalid_token`; `invalid_signature`, `invalid_issuer`, `invalid_audience` and `missing_claims` had no producer | projects/ores.security/src/jwt/jwt_authenticator.cpp | fixed | Found by writing the tests for the error codes the enum declares: the mapping searched the exception message for words such as "issuer" and "signature", and jwt-cpp's messages do not contain them, so every case fell through to `invalid_token`. Both paths now map from jwt-cpp's `token_verification_error` code and from the signature exception type, and each case has a test. The survey's proposal to delete `missing_claims` would have removed the case rather than noticed that none of them worked. |
| 2 | `validate_allow_expired` did not check the audience, so it accepted a token minted for another service as long as the signature and issuer matched | projects/ores.security/src/jwt/jwt_authenticator.cpp | fixed | Found by asking why the two validation paths differ: `validate` applies `with_issuer` and `with_audience`, and the allow-expired path applied only `with_issuer`. The refresh path is the one `ores.iam` uses to read an expired token, so the gap was reachable. It applies both now, with a test. |
| 3 | `get_n_parameter` cached its first answer in a function-local static, so once any test had hashed a password the production cost was unreachable in that process | projects/ores.security/src/crypto/password_hasher.cpp | fixed | Found while trying to pin the stored format: the test could not observe ln=14 because an earlier case had cached the test factor. The flag is read on every call now, and one test pins both costs. This is also what makes the verify floor meaningful, since the floor is the cost the build produces. |
| 4 | `crypto/encryption` had no consumer anywhere in the tree, and its `verify_password` returned true for empty ciphertext | projects/ores.security/include/ores.security/crypto/encryption.hpp | deleted with its test | The standing rule: no consumer, or a test-only consumer, is dead code. It is 432 lines with its test, and the empty-input defect went with it rather than being fixed in code nothing calls. |
| 5 | The first cut deleted `ores.security.hpp` as a consumer-less stub, which is wrong: every component keeps a namespace documentation header for doxygen | projects/ores.security/include/ores.security/ores.security.hpp | fixed by restoring it | Raised by the maintainer. The header's content is an empty namespace plus the `@brief` doxygen renders, and the diagram conventions read that `@brief` when they label the component, so a consumer census does not apply to it. The original also included one facet header while documenting three, which is why it read as a stub; it is restored without the include, with its claims corrected. The standard's G02 said dead headers go and did not exempt these, so G08 now states the exemption, and the same mistake on the ores.utility pass is corrected separately. |
| 6 | `jwt_authenticator_validate_invalid_hs256_signature` accepted either `invalid_signature` or `invalid_token`, an assertion weak enough to hide a regression in the mapping this pass fixed | projects/ores.security/tests/jwt_authenticator_tests.cpp | fixed | Raised in the PR review. The disjunction was written when the mapping could not tell the two apart, and it is exactly the hole the mapping fix closes, so the case now asserts `invalid_signature` alone. |
| 7 | Two record figures: the encryption implementation is 193 lines rather than 194, and work item 2 read as though the `base64` declarations were removed in a commit of their own | doc/agile/versions/v0/sprint_26/clean-security/task_clean_security.org | fixed | Raised by the independent verifier. The line count is corrected and work item 2 now says it landed in the same commit as item 1. |
11. Result
ores.security meets the standard for the items that apply to a model-free component. All ten work items landed across five `[security]` commits: the deletions, the JWT fixes, the hasher fixes, the overview and diagram, and the restored namespace header.
The component lost three files and 432 lines: an AES-256-GCM and PBKDF2 surface nothing referenced, its test, and two declarations that were never defined. It gained the tests the survey asked for and the answers to two questions the survey could not settle: the error codes the enum declares now have producers, and the refresh path now checks the audience the way the strict path does.
Its suite went from 127 assertions in 55 test cases to 149 in 57. Ten cases arrived, the eight that covered the deleted class left, and the surviving cases are stronger for the cost floor, the stored format, the empty-input refusal, and the four JWT failure paths.
Three things stay as recorded. `nbf` cannot be tested because `create_token` never sets it. `crypto/encryption` is gone rather than kept as an unused capability, on the standing rule. And the registry gap stands: no catalogue row and no models, so V06 is the accepted exception.
One correction to the record's own count: the fifth commit restores the namespace header the first commit should not have deleted, so the pass ends with five `[security]` commits rather than four.
12. Verification
An independent verifier checked the branch and the review met it. The verifier's round posted PASS on `285042a2ed`, and it did something the record had called impossible: rather than accept that the pre-change baseline could not be measured, it checked the base versions of the changed files out of `230006c221`, rebuilt, and measured 127 assertions in 55 test cases exactly, against 149 in 57 at the head. It proved all four JWT fixes load-bearing by reverting each separately (two, one and one failures), the scrypt floor by removing it and watching the control fail, and the cache claim by restoring the function-local static.
The PR review raised one more item, a weak assertion in `jwt_authenticator_validate_invalid_hs256_signature` that accepted either `invalid_signature` or `invalid_token`. Both are in Review rows 6 and 7 and fixed in `b7d6fadb75`, which the verifier's round did not cover because it post-dated the verdict.
PR #2162 merged as `b2e6c069db` on 2026-09-26 with every CI check green.