|
| 1 | +# OWASP ASVS V12 self-assessment + re-affirmation gate — design |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Make the library's TLS posture explicit and durable against silent drift. |
| 6 | +Three deliverables, one branch: |
| 7 | + |
| 8 | +1. A self-assessed mapping of the library against **OWASP ASVS 5.0 |
| 9 | + Level 2, chapter V12 (Secure Communication)**, committed at |
| 10 | + `docs/security/owasp-asvs.md`. Verbatim control text, per-control |
| 11 | + status, evidence linked to file paths. |
| 12 | +2. A README badge that names the standard and links to the checklist. |
| 13 | +3. A CI gate that fails any PR which changes TLS code without |
| 14 | + re-affirming the checklist (a dated audit-log entry). |
| 15 | + |
| 16 | +The branch also extracts `TlsHttpsConfigurator` from a nested class on |
| 17 | +`OpenApiServer` into its own file under |
| 18 | +`com.retailsvc.http.internal`, so the gate's file-scope predicate is |
| 19 | +precise. |
| 20 | + |
| 21 | +## Non-goals (v1) |
| 22 | + |
| 23 | +- **No external authority involvement.** The badge says "self-assessed |
| 24 | + against ASVS 5.0 L2"; there is no OWASP certification body. Wording |
| 25 | + must never say "Compliant" or "Certified". |
| 26 | +- **No new TLS features.** The ASVS L3 controls (OCSP stapling, ECH) |
| 27 | + are noted in the checklist as "Future" but not implemented here. |
| 28 | +- **No cipher allowlist.** V12.1.2 stays at Partial — JDK defaults are |
| 29 | + used. Curating an allowlist is a follow-up. |
| 30 | +- **No outbound/client-side coverage.** V12.3 (service-to-service) is |
| 31 | + marked N/A — the library is server-side termination only. |
| 32 | +- **No mTLS.** V12.1.3 is N/A — same reason. |
| 33 | +- **No automated re-affirmation.** The CI gate enforces *presence* of |
| 34 | + an audit-log line; the line itself is written by the contributor. |
| 35 | + Tempting alternative (auto-bump dates) defeats the "stop and think" |
| 36 | + purpose. |
| 37 | + |
| 38 | +## Public surface |
| 39 | + |
| 40 | +This branch changes **no public API**. The TlsHttpsConfigurator |
| 41 | +extraction is internal-package only. |
| 42 | + |
| 43 | +## Deliverable 1: `docs/security/owasp-asvs.md` |
| 44 | + |
| 45 | +### File header |
| 46 | + |
| 47 | +The file's first lines define scope and the immutable wording rule: |
| 48 | + |
| 49 | +```markdown |
| 50 | +# OWASP ASVS 5.0 Level 2 — self-assessment |
| 51 | + |
| 52 | +**Standard:** OWASP Application Security Verification Standard, version |
| 53 | +5.0.0, chapter V12 (Secure Communication). |
| 54 | +**Level:** 2 (typical baseline; consumers needing L3 must layer |
| 55 | +additional controls). |
| 56 | +**Scope:** Server-side TLS termination via `Builder.https(...)`. |
| 57 | +Outbound / service-to-service controls (V12.3) are N/A — the library |
| 58 | +makes no outbound TLS connections on the consumer's behalf. |
| 59 | + |
| 60 | +**Wording rules.** This document is a self-assessment; it does NOT |
| 61 | +claim certification or compliance. The README badge says "ASVS 5.0 |
| 62 | +Level 2" only — never "Certified", never "Compliant". OWASP does not |
| 63 | +issue conformance certifications. |
| 64 | + |
| 65 | +**Re-affirmation rule.** Any change to TLS-related code (see |
| 66 | +`.github/scripts/asvs-gate.sh` for the exact predicate) MUST append a |
| 67 | +dated entry to the [Audit log](#audit-log) in the same PR. CI |
| 68 | +enforces this. The contributor writes the line — automated bumps are |
| 69 | +not accepted. |
| 70 | + |
| 71 | +**Status legend.** |
| 72 | +- ✅ Implemented — the library satisfies this control end-to-end |
| 73 | +- 🤝 Delegated — consumer must satisfy; see Evidence for guidance |
| 74 | +- ⛔ N/A — out of scope for a server-side TLS termination library |
| 75 | +- 📋 Future — accepted as a gap; tracked for a follow-up release |
| 76 | +``` |
| 77 | + |
| 78 | +### Control table |
| 79 | + |
| 80 | +Verbatim ASVS 5.0 V12 controls applicable to this library's scope: |
| 81 | + |
| 82 | +| ID | Control (verbatim, ASVS 5.0) | Level | Status | Evidence | |
| 83 | +|---|---|---|---|---| |
| 84 | +| 12.1.1 | "Verify that only the latest recommended versions of the TLS protocol are enabled, such as TLS 1.2 and TLS 1.3." | L1 | ✅ Implemented | `TlsHttpsConfigurator` pins `setProtocols({"TLSv1.3","TLSv1.2"})`; verified end-to-end by `OpenApiServerHttpsIT#negotiatesTls13` | |
| 85 | +| 12.1.2 | "Verify that only recommended cipher suites are enabled, with the strongest cipher suites set as preferred." | L2 | 📋 Future | We rely on JDK 25 defaults (which exclude RC4, 3DES, EXPORT, NULL, anonymous suites by default). A curated allowlist with explicit preference order is tracked as a follow-up. | |
| 86 | +| 12.1.3 | "Verify that the application validates that mTLS client certificates are trusted before using the certificate identity." | L2 | ⛔ N/A | mTLS is not supported in v1. `TlsHttpsConfigurator` explicitly sets `setNeedClientAuth(false)` and `setWantClientAuth(false)`. If mTLS lands later, this row flips to Implemented + new evidence. | |
| 87 | +| 12.1.4 | "Verify that proper certification revocation, such as Online Certificate Status Protocol (OCSP) Stapling, is enabled." | L3 | 📋 Future | Out of scope for L2 baseline. Documented so L3-targeting consumers know the gap. | |
| 88 | +| 12.1.5 | "Verify that Encrypted Client Hello (ECH) is enabled in the application's TLS settings to prevent exposure of sensitive metadata." | L3 | 📋 Future | Out of scope for L2 baseline. JDK 25 has no stable ECH API. | |
| 89 | +| 12.2.1 | "Verify that TLS is used for all connectivity between a client and external facing, HTTP-based services, and does not fall back to insecure communications." | L1 | ✅ Implemented | When `.https(...)` is configured, the server binds `HttpsServer` only; no plaintext fallback listener is created. Mixed-mode (HTTP + HTTPS) is a documented non-goal — operators run two `OpenApiServer` instances if they need both. | |
| 90 | +| 12.2.2 | "Verify that external facing services use publicly trusted TLS certificates." | L1 | 🤝 Delegated | The library accepts whatever cert chain the consumer supplies. For production deployments, consumers MUST point `.https(certChain, privateKey)` at a chain signed by a publicly-trusted CA (Let's Encrypt is the recommended source, documented in README §HTTPS). | |
| 91 | +| 12.3.1–12.3.5 | (Service-to-service / outbound) | L2 | ⛔ N/A | The library does not initiate outbound TLS on the consumer's behalf. Consumers' outbound HTTP clients are their own responsibility. | |
| 92 | + |
| 93 | +### Per-control supplementary notes |
| 94 | + |
| 95 | +Each 🤝-Delegated row also gets a short prose paragraph after the |
| 96 | +table containing a code snippet showing how the consumer satisfies it. |
| 97 | +Example for 12.2.2: |
| 98 | + |
| 99 | +> **12.2.2 satisfaction guidance.** Operators should point |
| 100 | +> `.https(certChain, privateKey)` at a chain issued by a publicly |
| 101 | +> trusted CA. The recommended workflow is certbot / Let's Encrypt |
| 102 | +> with the PEM files mounted from a secret manager (see README |
| 103 | +> §HTTPS for the full deployment pattern). The library performs no |
| 104 | +> chain validation on its own certificate — the server merely |
| 105 | +> presents the chain — so it is on the operator to ensure |
| 106 | +> publicly-trusted issuance. |
| 107 | +
|
| 108 | +Future-status rows do NOT get supplementary code. They get a |
| 109 | +one-sentence "accepted gap" note and a link target (filled in when a |
| 110 | +follow-up issue is opened; the link can be a literal placeholder |
| 111 | +`<issue TBD>` only if no issue exists yet — the gate does NOT |
| 112 | +require live links). |
| 113 | + |
| 114 | +### Audit log section |
| 115 | + |
| 116 | +```markdown |
| 117 | +## Audit log |
| 118 | + |
| 119 | +- **2026-05-21** — Initial ASVS 5.0 Level 2 mapping for V12 controls (commit `<this commit SHA>`). All listed controls accepted as Implemented / Delegated / N/A / Future as tabulated above. |
| 120 | +``` |
| 121 | + |
| 122 | +The format is rigid and the gate parses it: `^- \*\*\d{4}-\d{2}-\d{2}\*\* — `. The bash regex used by the gate is `^[+]- \*\*[0-9]{4}-[0-9]{2}-[0-9]{2}\*\* — `. |
| 123 | + |
| 124 | +## Deliverable 2: Refactor `TlsHttpsConfigurator` out of `OpenApiServer` |
| 125 | + |
| 126 | +First commit on this branch. Pure refactor; tests stay green; no API |
| 127 | +change. |
| 128 | + |
| 129 | +**Create:** `src/main/java/com/retailsvc/http/internal/TlsHttpsConfigurator.java` |
| 130 | + |
| 131 | +```java |
| 132 | +package com.retailsvc.http.internal; |
| 133 | + |
| 134 | +import com.sun.net.httpserver.HttpsConfigurator; |
| 135 | +import com.sun.net.httpserver.HttpsParameters; |
| 136 | +import javax.net.ssl.SSLContext; |
| 137 | +import javax.net.ssl.SSLParameters; |
| 138 | + |
| 139 | +/** |
| 140 | + * Pins HTTPS to TLS 1.2 and 1.3 only, regardless of operator-level {@code java.security} |
| 141 | + * overrides, and explicitly leaves client-cert auth off (no mTLS in v1). |
| 142 | + */ |
| 143 | +public final class TlsHttpsConfigurator extends HttpsConfigurator { |
| 144 | + public TlsHttpsConfigurator(SSLContext context) { |
| 145 | + super(context); |
| 146 | + } |
| 147 | + |
| 148 | + @Override |
| 149 | + public void configure(HttpsParameters params) { |
| 150 | + SSLParameters sslParams = getSSLContext().getDefaultSSLParameters(); |
| 151 | + sslParams.setProtocols(new String[] {"TLSv1.3", "TLSv1.2"}); |
| 152 | + sslParams.setNeedClientAuth(false); |
| 153 | + sslParams.setWantClientAuth(false); |
| 154 | + params.setSSLParameters(sslParams); |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +**Modify:** `src/main/java/com/retailsvc/http/OpenApiServer.java` |
| 160 | + |
| 161 | +- Delete the nested `private static final class TlsHttpsConfigurator` |
| 162 | + and its imports of `SSLParameters` / `HttpsParameters`. |
| 163 | +- Add `import com.retailsvc.http.internal.TlsHttpsConfigurator;`. |
| 164 | +- The single call site `new TlsHttpsConfigurator(sslContext)` is |
| 165 | + unchanged. |
| 166 | + |
| 167 | +Visibility note: the constructor was implicitly private (nested in a |
| 168 | +public class with no modifier). After extraction it becomes `public` |
| 169 | +inside the `internal` package — same effective access for the only |
| 170 | +caller (`OpenApiServer`), which is in the parent package. The |
| 171 | +`internal` package is excluded from the published Javadoc and is not |
| 172 | +part of the API contract; this matches the existing precedent |
| 173 | +(`PemSslContext`, `Router`, `DispatchHandler`). |
| 174 | + |
| 175 | +## Deliverable 3: CI gate |
| 176 | + |
| 177 | +**Create:** `.github/workflows/asvs-gate.yml` |
| 178 | + |
| 179 | +```yaml |
| 180 | +name: OWASP ASVS gate |
| 181 | + |
| 182 | +on: |
| 183 | + pull_request: |
| 184 | + branches: [master] |
| 185 | + types: [opened, synchronize, reopened] |
| 186 | + |
| 187 | +permissions: |
| 188 | + contents: read |
| 189 | + pull-requests: read |
| 190 | + |
| 191 | +jobs: |
| 192 | + asvs-checklist: |
| 193 | + runs-on: ubuntu-latest |
| 194 | + steps: |
| 195 | + - uses: actions/checkout@v6 |
| 196 | + with: |
| 197 | + fetch-depth: 0 |
| 198 | + |
| 199 | + - name: Enforce ASVS re-affirmation on TLS code changes |
| 200 | + env: |
| 201 | + BASE_SHA: ${{ github.event.pull_request.base.sha }} |
| 202 | + HEAD_SHA: ${{ github.event.pull_request.head.sha }} |
| 203 | + run: .github/scripts/asvs-gate.sh |
| 204 | +``` |
| 205 | +
|
| 206 | +**Create:** `.github/scripts/asvs-gate.sh` (bash, mode 0755). |
| 207 | + |
| 208 | +Logic: |
| 209 | + |
| 210 | +1. `git diff --name-only "$BASE_SHA" "$HEAD_SHA"` → changed file list. |
| 211 | +2. **TLS-relevance predicate** (OR-ed): |
| 212 | + - Path match: any changed file matches the regex |
| 213 | + `^src/main/java/com/retailsvc/http/internal/.*(Ssl|Tls|Https).*\.java$`. |
| 214 | + - Import diff: `git diff -U0 "$BASE_SHA" "$HEAD_SHA" -- 'src/main/java/**/*.java'` |
| 215 | + produces any line matching |
| 216 | + `^[+-]import (javax\.net\.ssl\.|com\.sun\.net\.httpserver\.Https)`. |
| 217 | +3. If predicate is false → `echo "ASVS gate: no TLS-relevant changes"; exit 0`. |
| 218 | +4. If predicate is true and `docs/security/owasp-asvs.md` is NOT in |
| 219 | + the changed file list → fail with the message in the brainstorming |
| 220 | + section. Print the triggering files. |
| 221 | +5. If predicate is true and the file IS changed, check the diff of |
| 222 | + `docs/security/owasp-asvs.md` for at least one added line matching |
| 223 | + `^[+]- \*\*[0-9]{4}-[0-9]{2}-[0-9]{2}\*\* —`. If none → fail with a |
| 224 | + message explaining the audit-log line format and showing an example. |
| 225 | +6. On success → `echo "ASVS gate: TLS-relevant changes re-affirmed in |
| 226 | + docs/security/owasp-asvs.md"; exit 0`. |
| 227 | + |
| 228 | +### Failure message format |
| 229 | + |
| 230 | +Each failure path prints, to stderr: |
| 231 | + |
| 232 | +``` |
| 233 | +::error title=ASVS V12 gate::TLS-relevant code changed but docs/security/owasp-asvs.md was not updated. |
| 234 | +
|
| 235 | +Triggered by changes to: |
| 236 | + <one path per line> |
| 237 | +
|
| 238 | +Required action: |
| 239 | + 1. Open docs/security/owasp-asvs.md |
| 240 | + 2. Confirm each ASVS 5.0 L2 control still holds (update Status / Evidence rows if not) |
| 241 | + 3. Append a dated line to ## Audit log, e.g.: |
| 242 | + - **2026-06-12** — Re-affirmed after change to PemSslContext.java (this PR); all controls hold |
| 243 | +
|
| 244 | +This gate exists so TLS changes can't silently drift away from the documented controls. |
| 245 | +See docs/security/owasp-asvs.md for the policy. |
| 246 | +``` |
| 247 | + |
| 248 | +The `::error::` annotation makes it surface in the PR Files-changed |
| 249 | +view next to the failing check, not just in the run log. |
| 250 | + |
| 251 | +### Script testability |
| 252 | + |
| 253 | +`asvs-gate.sh` runs cleanly outside CI for local pre-push validation: |
| 254 | + |
| 255 | +```bash |
| 256 | +BASE_SHA=$(git merge-base origin/master HEAD) HEAD_SHA=HEAD .github/scripts/asvs-gate.sh |
| 257 | +``` |
| 258 | + |
| 259 | +Document this invocation in a short header comment at the top of the |
| 260 | +script. No bats / shunit2 framework dependency — the logic is shallow |
| 261 | +enough that one shell-level smoke test (run on the OWASP branch |
| 262 | +itself, which triggers all paths) is sufficient confidence. |
| 263 | + |
| 264 | +## Deliverable 4: README badge |
| 265 | + |
| 266 | +**Modify:** `README.md`. |
| 267 | + |
| 268 | +In the existing badge block (between the Sonar/coverage badges and the |
| 269 | +workflow badge), insert one line: |
| 270 | + |
| 271 | +```markdown |
| 272 | +[](docs/security/owasp-asvs.md) |
| 273 | +``` |
| 274 | + |
| 275 | +Hover text and the link target both point at the local file |
| 276 | +`docs/security/owasp-asvs.md`. Relative path so it works on GitHub |
| 277 | +web, raw clones, and IDE preview. |
| 278 | + |
| 279 | +No other README changes in this branch. The HTTPS section already |
| 280 | +exists from the previous branch; references to ASVS within it are a |
| 281 | +nice-to-have but not required (and would couple the two docs more |
| 282 | +than necessary). |
| 283 | + |
| 284 | +## File layout summary |
| 285 | + |
| 286 | +``` |
| 287 | +.github/ |
| 288 | + scripts/ |
| 289 | + asvs-gate.sh [new, executable] |
| 290 | + workflows/ |
| 291 | + asvs-gate.yml [new] |
| 292 | +docs/ |
| 293 | + security/ |
| 294 | + owasp-asvs.md [new] |
| 295 | +README.md [modified — one badge line] |
| 296 | +src/main/java/com/retailsvc/http/ |
| 297 | + OpenApiServer.java [modified — nested class extracted out] |
| 298 | + internal/ |
| 299 | + TlsHttpsConfigurator.java [new] |
| 300 | +``` |
| 301 | +
|
| 302 | +## Order of commits |
| 303 | +
|
| 304 | +1. `refactor: Extract TlsHttpsConfigurator to internal package` |
| 305 | +2. `chore: Add OWASP ASVS 5.0 L2 self-assessment for V12` |
| 306 | +3. `ci: Add ASVS re-affirmation gate for TLS code changes` |
| 307 | +4. `docs: Add OWASP ASVS badge to README` |
| 308 | +
|
| 309 | +Order matters: the refactor lands first so the gate (commit 3) can be |
| 310 | +authored against the new file layout. The badge lands last so the |
| 311 | +linked file already exists. |
| 312 | +
|
| 313 | +## Acceptance criteria |
| 314 | +
|
| 315 | +- `docs/security/owasp-asvs.md` exists, with verbatim ASVS 5.0 V12 |
| 316 | + control text, statuses, evidence, audit log with an initial dated |
| 317 | + entry. |
| 318 | +- `src/main/java/com/retailsvc/http/internal/TlsHttpsConfigurator.java` |
| 319 | + exists; `OpenApiServer.java` no longer contains a nested |
| 320 | + `TlsHttpsConfigurator`; full test suite (`mvn verify`) is green. |
| 321 | +- `.github/workflows/asvs-gate.yml` runs on PR open / sync / reopen. |
| 322 | +- `.github/scripts/asvs-gate.sh` is executable, passes its own |
| 323 | + re-affirmation when run against the OWASP branch (which by |
| 324 | + definition both modifies a file in the import-grep predicate set |
| 325 | + *and* updates `docs/security/owasp-asvs.md`). |
| 326 | +- README badge renders and the link resolves on the GitHub web UI. |
| 327 | +- The OWASP branch passes its own gate when targeted at master. |
| 328 | +
|
| 329 | +## Out-of-scope follow-ups (post-v1) |
| 330 | +
|
| 331 | +- **Cipher suite allowlist** — flip 12.1.2 to Implemented; curated |
| 332 | + list of TLS 1.3 + TLS 1.2 suites with forward secrecy preferred. |
| 333 | +- **mTLS support** — flip 12.1.3 to Implemented; new builder method |
| 334 | + `.requireClientCert(TrustManager)`. |
| 335 | +- **OCSP stapling** — flip 12.1.4 (L3) to Implemented; requires |
| 336 | + `SSLEngine` customisation that JDK `HttpsServer` doesn't expose |
| 337 | + cleanly, likely needs a backend adapter (Jetty / Helidon Níma). |
| 338 | +- **L3 baseline** — once the L3 controls above land, document a |
| 339 | + parallel L3 self-assessment column or a separate L3-targeted |
| 340 | + checklist file. |
| 341 | +- **Cross-version mapping** — if auditors ask for ASVS 4.0.3 mapping, |
| 342 | + add a second column mapping each 5.0 control back to its 4.0.3 |
| 343 | + predecessor. |
| 344 | +- **Multi-chapter coverage** — V11 (Cryptography) and V13 |
| 345 | + (Configuration) have controls the library partially touches; out of |
| 346 | + scope for this branch which targets V12 only. |
0 commit comments